Subversion Repositories SmartDukaan

Rev

Rev 36621 | Rev 36642 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36621 Rev 36632
Line 52... Line 52...
52
		alert('Select a user');
52
		alert('Select a user');
53
		return;
53
		return;
54
	}
54
	}
55
	state.authUserId = parseInt(uid);
55
	state.authUserId = parseInt(uid);
56
	state.categoryId = parseInt($('#bp-category').val());
56
	state.categoryId = parseInt($('#bp-category').val());
-
 
57
    state.mode = 'list'; // 'list' or 'create' or 'view'
57
	$('#bp-user-label').text($('#bp-auth-user option:selected').text());
58
	$('#bp-user-label').text($('#bp-auth-user option:selected').text());
58
 
59
 
59
	// Check home location
60
    // Load home location
60
	$.ajax({
61
	$.ajax({
61
		url: context + "/beatPlan/getBaseLocation", type: "GET", dataType: "json",
62
		url: context + "/beatPlan/getBaseLocation", type: "GET", dataType: "json",
62
		data: {authUserId: uid},
63
		data: {authUserId: uid},
63
		success: function (r) {
64
		success: function (r) {
64
			var data = r.response || r;
65
			var data = r.response || r;
65
			if (data.locationName) {
66
			if (data.locationName) {
66
				state.homeLat = parseFloat(data.latitude);
67
				state.homeLat = parseFloat(data.latitude);
67
				state.homeLng = parseFloat(data.longitude);
68
				state.homeLng = parseFloat(data.longitude);
68
				state.homeName = data.locationName;
69
				state.homeName = data.locationName;
-
 
70
            }
-
 
71
            showBeatList();
-
 
72
        }
-
 
73
    });
-
 
74
});
-
 
75
 
-
 
76
// ============ BEAT LIST VIEW ============
-
 
77
function showBeatList() {
-
 
78
    state.mode = 'list';
-
 
79
    $('#bottom-bar').hide();
-
 
80
 
-
 
81
    // Load all beats for this user
-
 
82
    var now = new Date();
-
 
83
    var month = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
-
 
84
 
-
 
85
    $.ajax({
-
 
86
        url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
-
 
87
        data: {authUserId: state.authUserId, month: month},
-
 
88
        success: function (r) {
-
 
89
            var data = r.response || r;
-
 
90
            var beats = data.scheduledBeats || [];
-
 
91
 
-
 
92
            var html = '<div style="margin-bottom:10px;">';
-
 
93
            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>';
-
 
94
            html += '</div>';
-
 
95
 
-
 
96
            if (beats.length === 0) {
-
 
97
                html += '<p style="color:#64748b;font-size:12px;text-align:center;padding:20px;">No beats created yet.</p>';
-
 
98
            }
-
 
99
 
-
 
100
            // Deduplicate by beatName
-
 
101
            var seen = {};
-
 
102
            beats.forEach(function (b) {
-
 
103
                var key = b.beatName || b.planGroupId;
-
 
104
                if (!seen[key]) {
-
 
105
                    seen[key] = {beat: b, count: 0};
-
 
106
                }
-
 
107
                seen[key].count++;
-
 
108
            });
-
 
109
 
-
 
110
            Object.keys(seen).forEach(function (key) {
-
 
111
                var item = seen[key];
-
 
112
                var b = item.beat;
-
 
113
                var totalV = 0, totalKm = 0;
-
 
114
                b.days.forEach(function (d) {
-
 
115
                    totalV += d.visitCount || 0;
-
 
116
                    totalKm += d.totalKm || 0;
-
 
117
                });
-
 
118
 
-
 
119
                var statusCls = b.status === 'running' ? 'status-running' : b.status === 'completed' ? 'status-completed' : 'status-scheduled';
-
 
120
                var statusLabel = b.status === 'running' ? 'Live' : b.status === 'completed' ? 'Done' : b.status === 'unscheduled' ? 'Unscheduled' : 'Scheduled';
-
 
121
 
-
 
122
                html += '<div class="beat-card beat-list-item" data-plangroup="' + b.planGroupId + '" style="cursor:pointer;">';
69
				loadPartners();
123
                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>';
-
 
124
                html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km';
-
 
125
                if (item.count > 1) html += ' | ' + item.count + 'x scheduled';
-
 
126
                html += '</div>';
-
 
127
                html += '</div>';
-
 
128
            });
-
 
129
 
-
 
130
            $('#route-list').html(html);
70
			} else {
131
        }
-
 
132
    });
-
 
133
}
-
 
134
 
-
 
135
// Click on existing beat → view its route on map
-
 
136
$(document).on('click', '.beat-list-item', function () {
-
 
137
    var pg = $(this).data('plangroup');
-
 
138
    viewBeat(pg);
-
 
139
});
-
 
140
 
-
 
141
function viewBeat(planGroupId) {
-
 
142
    state.mode = 'view';
-
 
143
    $('#bottom-bar').hide();
-
 
144
 
-
 
145
    // Load partners first (for coordinates), then load beat visits
-
 
146
    $.ajax({
-
 
147
        url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
-
 
148
        data: {
-
 
149
            authUserId: state.authUserId,
-
 
150
            categoryId: state.categoryId,
-
 
151
            startLat: state.homeLat,
-
 
152
            startLng: state.homeLng
-
 
153
        },
-
 
154
        success: function (r) {
-
 
155
            var pData = r.response || r;
-
 
156
            state.partners = pData.partners || [];
-
 
157
 
-
 
158
            // Build partner lookup by fofoId
-
 
159
            var partnerMap = {};
-
 
160
            state.partners.forEach(function (p) {
-
 
161
                partnerMap[p.fofoId] = p;
-
 
162
            });
-
 
163
 
-
 
164
            // Load beat visits
-
 
165
            $.ajax({
-
 
166
                url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
-
 
167
                data: {authUserId: state.authUserId, month: '2020-01'},
-
 
168
                success: function (r2) {
-
 
169
                    var calData = r2.response || r2;
-
 
170
                    var beat = (calData.scheduledBeats || []).find(function (b) {
-
 
171
                        return b.planGroupId === planGroupId;
-
 
172
                    });
-
 
173
                    if (!beat) {
-
 
174
                        alert('Beat not found');
-
 
175
                        return;
-
 
176
                    }
-
 
177
 
-
 
178
                    // Reconstruct state.days from beat data for map drawing
-
 
179
                    // We need actual visit fofoIds — fetch from beat_plan records
-
 
180
                    $.ajax({
-
 
181
                        url: context + "/beatPlan/getBeatVisits", type: "GET", dataType: "json",
-
 
182
                        data: {planGroupId: planGroupId},
-
 
183
                        success: function (r3) {
-
 
184
                            var visits = (r3.response || r3) || [];
-
 
185
 
-
 
186
                            // Group visits by dayNumber
-
 
187
                            var dayVisitsMap = {};
-
 
188
                            visits.forEach(function (v) {
-
 
189
                                if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
-
 
190
                                dayVisitsMap[v.dayNumber].push(v);
-
 
191
                            });
-
 
192
 
-
 
193
                            // Build days for route display
-
 
194
                            state.days = [];
-
 
195
                            beat.days.forEach(function (d) {
-
 
196
                                var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
-
 
197
                                    return a.sequenceOrder - b.sequenceOrder;
-
 
198
                                });
-
 
199
                                var startLat = state.homeLat, startLng = state.homeLng, startName = state.homeName;
-
 
200
 
-
 
201
                                // If not first day, start from previous day's last visit
-
 
202
                                if (state.days.length > 0) {
-
 
203
                                    var prevDay = state.days[state.days.length - 1];
-
 
204
                                    if (prevDay.visits.length > 0 && prevDay.endAction === 'DAYBREAK') {
-
 
205
                                        var lastV = prevDay.visits[prevDay.visits.length - 1];
-
 
206
                                        startLat = lastV.lat;
-
 
207
                                        startLng = lastV.lng;
-
 
208
                                        startName = lastV.code || lastV.name;
-
 
209
                                    }
-
 
210
                                }
-
 
211
 
-
 
212
                                var builtVisits = [];
-
 
213
                                dayVisits.forEach(function (v) {
-
 
214
                                    var p = partnerMap[v.fofoId];
-
 
215
                                    if (p) {
-
 
216
                                        builtVisits.push({
-
 
217
                                            id: p.fofoId, type: 'partner',
-
 
218
                                            name: p.code + ' - ' + (p.outletName || p.businessName || ''),
-
 
219
                                            code: p.code,
-
 
220
                                            lat: p.latitude ? parseFloat(p.latitude) : null,
-
 
221
                                            lng: p.longitude ? parseFloat(p.longitude) : null
-
 
222
                                        });
-
 
223
                                    }
-
 
224
                                });
-
 
225
 
-
 
226
                                state.days.push({
-
 
227
                                    dayNumber: d.dayNumber,
-
 
228
                                    startLat: startLat,
-
 
229
                                    startLng: startLng,
-
 
230
                                    startName: startName,
-
 
231
                                    visits: builtVisits,
-
 
232
                                    endAction: d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME',
-
 
233
                                    totalKm: d.totalKm || 0,
-
 
234
                                    totalMins: d.totalMins || 0
-
 
235
                                });
-
 
236
                            });
-
 
237
 
-
 
238
                            state.currentDay = 1;
-
 
239
 
-
 
240
                            // Render left panel
71
				showHomeModal();
241
                            var html = '<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back to list</button></div>';
-
 
242
                            html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + beat.beatName + '</div>';
-
 
243
                            $('#route-list').html(html);
-
 
244
 
-
 
245
                            // Use renderRoutePanel for consistent display (but without remove/daybreak buttons)
-
 
246
                            state.mode = 'view';
-
 
247
                            renderRoutePanel();
-
 
248
                            // Prepend the back button + title before route days
72
			}
249
                            $('#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:10px;">' + beat.beatName + '</div>');
-
 
250
 
-
 
251
                            // Init map with all partners + highlight beat route
-
 
252
                            initMap();
-
 
253
                            resetMarkerColors();
-
 
254
                            drawRoute();
-
 
255
                        }
-
 
256
                    });
-
 
257
                }
-
 
258
            });
-
 
259
        }
-
 
260
    });
73
		}
261
}
-
 
262
 
-
 
263
// Back to list
-
 
264
$(document).on('click', '.btn-back-to-list', function () {
-
 
265
    showBeatList();
-
 
266
    // Clear map
-
 
267
    if (map) {
-
 
268
        for (var key in markers) {
-
 
269
            markers[key].setMap(null);
-
 
270
        }
-
 
271
        markers = {};
-
 
272
        clearRoute();
-
 
273
    }
74
	});
274
});
-
 
275
 
-
 
276
// New Beat button
-
 
277
$(document).on('click', '#btn-new-beat', function () {
-
 
278
    var beatTitle = prompt('Enter beat name:', '');
-
 
279
    if (!beatTitle || !beatTitle.trim()) return;
-
 
280
 
-
 
281
    state.beatTitle = beatTitle.trim();
-
 
282
    state.mode = 'create';
-
 
283
 
-
 
284
    if (!state.homeLat) {
-
 
285
        showHomeModal();
-
 
286
    } else {
-
 
287
        loadPartners();
-
 
288
    }
75
});
289
});
76
 
290
 
77
// ============ HOME LOCATION MODAL ============
291
// ============ HOME LOCATION MODAL ============
78
var homeMap, homeMapMarker;
292
var homeMap, homeMapMarker;
79
 
293
 
Line 156... Line 370...
156
	state.days = [{
370
	state.days = [{
157
		dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
371
		dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
158
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
372
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
159
	}];
373
	}];
160
	state.currentDay = 1;
374
	state.currentDay = 1;
161
	$('#bottom-bar').show();
375
    if (state.mode === 'create') $('#bottom-bar').show();
162
	renderRoutePanel();
376
	renderRoutePanel();
163
}
377
}
164
 
378
 
165
function initMap() {
379
function initMap() {
166
	var center = {lat: state.homeLat, lng: state.homeLng};
380
	var center = {lat: state.homeLat, lng: state.homeLng};
Line 286... Line 500...
286
	html += '<h4>' + partner.code + ' - ' + name + '</h4>';
500
	html += '<h4>' + partner.code + ' - ' + name + '</h4>';
287
	html += '<div class="popup-meta">' + addr + '</div>';
501
	html += '<div class="popup-meta">' + addr + '</div>';
288
	html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + 'min visit</div>';
502
	html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + 'min visit</div>';
289
	html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
503
	html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
290
	html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
504
	html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
291
	html += '<button class="popup-nextday" onclick="addVisit(' + partner.fofoId + ',\'nextday\')">Continue Next Day</button>';
-
 
292
	html += '</div>';
505
	html += '</div>';
293
 
506
 
294
	infoWindow.setContent(html);
507
	infoWindow.setContent(html);
295
	infoWindow.open(map, marker);
508
	infoWindow.open(map, marker);
296
}
509
}
Line 434... Line 647...
434
		l.setMap(null);
647
		l.setMap(null);
435
	});
648
	});
436
	distLabels = [];
649
	distLabels = [];
437
}
650
}
438
 
651
 
-
 
652
var DAY_ROUTE_COLORS = ['#3b82f6', '#a855f7', '#06b6d4', '#ec4899', '#f59e0b', '#14b8a6'];
-
 
653
 
439
function drawRoute() {
654
function drawRoute() {
440
	clearRoute();
655
	clearRoute();
441
	var day = curDay();
-
 
442
	var pts = [{lat: day.startLat, lng: day.startLng}];
-
 
443
	day.visits.forEach(function (v) {
-
 
444
		if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
-
 
445
	});
-
 
446
 
656
 
447
	for (var i = 0; i < pts.length - 1; i++) {
657
    // Draw routes for ALL days, not just current
-
 
658
    state.days.forEach(function (day, dayIdx) {
448
		var line = new google.maps.Polyline({
659
        var isCurrent = day.dayNumber === state.currentDay;
-
 
660
        var routeColor = DAY_ROUTE_COLORS[dayIdx % DAY_ROUTE_COLORS.length];
449
			path: [pts[i], pts[i + 1]],
661
        var opacity = isCurrent ? 0.9 : 0.5;
450
			geodesic: true,
662
        var weight = isCurrent ? 3 : 2;
-
 
663
 
451
			strokeColor: '#3b82f6',
664
        var pts = [{lat: day.startLat, lng: day.startLng}];
452
			strokeOpacity: 0.9,
665
        day.visits.forEach(function (v) {
453
			strokeWeight: 3,
666
            if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
454
			map: map
667
        });
-
 
668
 
-
 
669
        // Draw visit route lines for this day
-
 
670
        for (var i = 0; i < pts.length - 1; i++) {
-
 
671
            var line = new google.maps.Polyline({
-
 
672
                path: [pts[i], pts[i + 1]], geodesic: true,
-
 
673
                strokeColor: routeColor, strokeOpacity: opacity, strokeWeight: weight, map: map
455
		});
674
            });
456
		routeLines.push(line);
675
            routeLines.push(line);
457
 
676
 
-
 
677
            // Only show distance labels on current day
-
 
678
            if (isCurrent) {
458
		var d = haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
679
                var d = haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
459
		var mid = {lat: (pts[i].lat + pts[i + 1].lat) / 2, lng: (pts[i].lng + pts[i + 1].lng) / 2};
680
                var mid = {lat: (pts[i].lat + pts[i + 1].lat) / 2, lng: (pts[i].lng + pts[i + 1].lng) / 2};
460
		var lbl = new google.maps.Marker({
681
                var lbl = new google.maps.Marker({
461
			position: mid, map: map, icon: {path: google.maps.SymbolPath.CIRCLE, scale: 0},
682
                    position: mid, map: map, icon: {path: google.maps.SymbolPath.CIRCLE, scale: 0},
462
			label: {text: d.toFixed(1) + 'km', color: '#93c5fd', fontSize: '10px', fontWeight: '600'}
683
                    label: {text: d.toFixed(1) + 'km', color: '#93c5fd', fontSize: '10px', fontWeight: '600'}
463
		});
684
                });
464
		distLabels.push(lbl);
685
                distLabels.push(lbl);
-
 
686
            }
-
 
687
        }
465
	}
688
 
-
 
689
        // Draw return-to-home line (not on DAYBREAK days)
-
 
690
        if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
-
 
691
            var lastV = day.visits[day.visits.length - 1];
-
 
692
            if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
-
 
693
                var retLine = new google.maps.Polyline({
-
 
694
                    path: [{lat: lastV.lat, lng: lastV.lng}, {lat: state.homeLat, lng: state.homeLng}],
-
 
695
                    geodesic: true, strokeColor: '#22c55e', strokeOpacity: isCurrent ? 0.4 : 0.2, strokeWeight: 2,
-
 
696
                    icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
-
 
697
                    map: map
-
 
698
                });
-
 
699
                routeLines.push(retLine);
-
 
700
            }
-
 
701
        }
-
 
702
    });
466
}
703
}
467
 
704
 
468
function recalcDay() {
705
function recalcDay() {
469
	var day = curDay();
706
	var day = curDay();
470
	var pts = [{lat: day.startLat, lng: day.startLng}];
707
	var pts = [{lat: day.startLat, lng: day.startLng}];
Line 492... Line 729...
492
}
729
}
493
 
730
 
494
// ============ ROUTE PANEL ============
731
// ============ ROUTE PANEL ============
495
function renderRoutePanel() {
732
function renderRoutePanel() {
496
	var html = '';
733
	var html = '';
-
 
734
    if (state.mode === 'create' && state.beatTitle) {
-
 
735
        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>';
-
 
736
        html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + state.beatTitle + '</div>';
-
 
737
    }
497
	state.days.forEach(function (day) {
738
	state.days.forEach(function (day) {
498
		var isCurrent = day.dayNumber === state.currentDay;
739
		var isCurrent = day.dayNumber === state.currentDay;
-
 
740
        var isLastDay = day.dayNumber === state.days.length;
499
		html += '<div class="route-day">';
741
		html += '<div class="route-day">';
500
		html += '<div class="route-day-header">';
742
		html += '<div class="route-day-header">';
501
		html += '<span>Day ' + day.dayNumber + (isCurrent ? ' (current)' : '') + '</span>';
743
		html += '<span>Day ' + day.dayNumber + (isCurrent ? ' (current)' : '') + '</span>';
502
		html += '<span>' + fmtMins(day.totalMins) + '</span>';
744
		html += '<span>' + fmtMins(day.totalMins) + '</span>';
503
		html += '</div>';
745
		html += '</div>';
504
 
746
 
505
		// Start point
747
		// Start point
506
		html += '<div class="route-stop route-stop-home"><span class="stop-num">H</span><div class="stop-info"><div class="stop-name">' + (day.startName || 'Start') + '</div></div></div>';
748
        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>';
507
 
749
 
508
		// Visits with distance between each
750
		// Visits with distance between each
509
		var prevLat = day.startLat, prevLng = day.startLng;
751
		var prevLat = day.startLat, prevLng = day.startLng;
-
 
752
        var isLastStop;
510
		day.visits.forEach(function (v, i) {
753
		day.visits.forEach(function (v, i) {
511
			// Distance connector from previous point
754
            isLastStop = (i === day.visits.length - 1);
-
 
755
 
512
			var segDist = 0, segTime = 0;
756
			var segDist = 0, segTime = 0;
513
			if (prevLat && prevLng && v.lat && v.lng) {
757
			if (prevLat && prevLng && v.lat && v.lng) {
514
				segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
758
				segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
515
				segTime = (segDist / AVG_SPEED) * 60;
759
				segTime = (segDist / AVG_SPEED) * 60;
516
			}
760
			}
Line 519... Line 763...
519
			}
763
			}
520
 
764
 
521
			html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;">';
765
			html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;">';
522
			html += '<span class="stop-num">' + (i + 1) + '</span>';
766
			html += '<span class="stop-num">' + (i + 1) + '</span>';
523
			html += '<div class="stop-info"><div class="stop-name">' + (v.code || '') + '</div>';
767
			html += '<div class="stop-info"><div class="stop-name">' + (v.code || '') + '</div>';
524
			html += '<div class="stop-meta">' + (v.name || '') + '</div></div>';
768
            html += '<div class="stop-meta">' + (v.name || '') + '</div>';
-
 
769
 
-
 
770
            // Show "Last Visit" + "Day Break" buttons on last stop of current day
-
 
771
            if (isLastStop && isCurrent && !day.endAction) {
-
 
772
                html += '<div style="margin-top:4px;display:flex;gap:4px;">';
-
 
773
                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>';
-
 
774
                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>';
-
 
775
                html += '</div>';
-
 
776
            }
-
 
777
 
-
 
778
            html += '</div>';
-
 
779
            if (state.mode === 'create') {
525
			html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">&times;</span>';
780
                html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">&times;</span>';
-
 
781
            }
526
			html += '</div>';
782
			html += '</div>';
527
 
783
 
528
			prevLat = v.lat;
784
			prevLat = v.lat;
529
			prevLng = v.lng;
785
			prevLng = v.lng;
530
		});
786
		});
531
 
787
 
532
		// Return home distance
788
        // Show return-to-home only when day is complete (Last Visit) or still in progress
533
		if (day.endAction === 'HOME' && day.visits.length > 0) {
789
        if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
534
			var lastV = day.visits[day.visits.length - 1];
790
			var lastV = day.visits[day.visits.length - 1];
535
			var retDist = 0, retTime = 0;
791
			var retDist = 0, retTime = 0;
536
			if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
792
			if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
537
				retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
793
				retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
538
				retTime = (retDist / AVG_SPEED) * 60;
794
				retTime = (retDist / AVG_SPEED) * 60;
539
			}
795
			}
540
			if (retDist > 0) {
796
			if (retDist > 0) {
541
				html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">' + retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + '</span></div>';
797
                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>';
542
			}
798
			}
543
			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>';
799
			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>';
544
		}
800
		}
545
 
801
 
-
 
802
        // Day break indicator
-
 
803
        if (day.endAction === 'DAYBREAK' && day.visits.length > 0) {
-
 
804
            html += '<div style="margin-left:12px;padding:4px 8px;font-size:11px;color:#f59e0b;font-weight:600;">-- Day Break (stays near last visit) --</div>';
-
 
805
        }
-
 
806
 
546
		html += '<div class="day-summary">' + day.visits.length + ' stops | ' + day.totalKm.toFixed(1) + ' km</div>';
807
		html += '<div class="day-summary">' + day.visits.length + ' stops | ' + day.totalKm.toFixed(1) + ' km</div>';
547
		html += '</div>';
808
		html += '</div>';
548
	});
809
	});
549
 
810
 
550
	$('#route-list').html(html);
811
	$('#route-list').html(html);
Line 608... Line 869...
608
	drawRoute();
869
	drawRoute();
609
	renderRoutePanel();
870
	renderRoutePanel();
610
	updateBottomBar();
871
	updateBottomBar();
611
});
872
});
612
 
873
 
613
// ============ END DAY / FINISH ============
874
// ============ LAST VISIT (from sidebar) — marks beat as complete, return home ============
614
$('#btn-end-day').on('click', function () {
875
$(document).on('click', '.btn-last-visit', function (e) {
615
	var day = curDay();
876
    e.stopPropagation();
616
	if (day.visits.length === 0) {
877
    var dayNum = parseInt($(this).data('daynum'));
617
		alert('Add at least one stop');
878
    var day = state.days[dayNum - 1];
618
		return;
879
    if (!day || day.visits.length === 0) return;
619
	}
-
 
620
 
880
 
-
 
881
    var lastVisit = day.visits[day.visits.length - 1];
621
	$('#daybreak-info').text('Day ' + day.dayNumber + ': ' + day.visits.length + ' stops, ' + day.totalKm.toFixed(1) + ' km, ' + fmtMins(day.totalMins));
882
    if (!confirm('Mark ' + (lastVisit.code || lastVisit.name) + ' as last visit? Beat will end here and return home.')) return;
622
	$('#modal-daybreak').addClass('show');
-
 
623
});
-
 
624
 
883
 
625
$('input[name="stay"]').on('change', function () {
-
 
626
	$('#hotel-fields').toggle($(this).val() === 'HOTEL');
-
 
627
	if ($(this).val() === 'HOTEL') {
884
    day.endAction = 'HOME';
628
		setTimeout(function () {
-
 
629
			if (!window._hotelMap) {
-
 
630
				window._hotelMap = new google.maps.Map(document.getElementById('hotel-map'), {
-
 
631
					center: {
-
 
632
						lat: 28.6,
-
 
633
						lng: 77.2
-
 
634
					}, zoom: 6
885
    drawRoute();
635
				});
-
 
636
				var hm = new google.maps.Marker({map: window._hotelMap, draggable: true});
-
 
637
				var hac = new google.maps.places.Autocomplete(document.getElementById('hotel-search'), {componentRestrictions: {country: 'in'}});
-
 
638
				hac.addListener('place_changed', function () {
-
 
639
					var p = hac.getPlace();
886
    renderRoutePanel();
640
					if (!p.geometry) return;
887
    updateBottomBar();
641
					window._hotelMap.setCenter(p.geometry.location);
-
 
642
					window._hotelMap.setZoom(14);
-
 
643
					hm.setPosition(p.geometry.location);
-
 
644
					state._hotelLat = p.geometry.location.lat();
-
 
645
					state._hotelLng = p.geometry.location.lng();
-
 
646
					state._hotelName = p.name || p.formatted_address;
-
 
647
				});
-
 
648
				hm.addListener('dragend', function () {
-
 
649
					state._hotelLat = hm.getPosition().lat();
-
 
650
					state._hotelLng = hm.getPosition().lng();
-
 
651
				});
-
 
652
			}
-
 
653
		}, 200);
-
 
654
	}
-
 
655
});
888
});
656
 
889
 
-
 
890
// ============ DAY BREAK (from sidebar) — end day, next day starts from last visit ============
657
$('#daybreak-cancel').on('click', function () {
891
$(document).on('click', '.btn-day-break', function (e) {
-
 
892
    e.stopPropagation();
658
	$('#modal-daybreak').removeClass('show');
893
    var dayNum = parseInt($(this).data('daynum'));
-
 
894
    var day = state.days[dayNum - 1];
-
 
895
    if (!day || day.visits.length === 0) return;
659
});
896
 
660
$('#daybreak-confirm').on('click', function () {
897
    var lastVisit = day.visits[day.visits.length - 1];
-
 
898
    if (!confirm('Day Break at ' + (lastVisit.code || lastVisit.name) + '? Next day will start from this location.')) return;
-
 
899
 
661
	var day = curDay();
900
    day.endAction = 'DAYBREAK';
-
 
901
 
-
 
902
    // Next day starts from LAST VISIT location (not home)
662
	var opt = $('input[name="stay"]:checked').val();
903
    var nextNum = state.days.length + 1;
-
 
904
    state.days.push({
-
 
905
        dayNumber: nextNum,
-
 
906
        startLat: lastVisit.lat, startLng: lastVisit.lng, startName: lastVisit.code || lastVisit.name,
-
 
907
        visits: [], endAction: null,
-
 
908
        stayName: null, stayLat: null, stayLng: null,
-
 
909
        totalKm: 0, totalMins: 0
-
 
910
    });
663
	day.endAction = opt;
911
    state.currentDay = nextNum;
664
 
912
 
665
	if (opt === 'HOME') {
913
    resetMarkerColors();
666
		day.stayLat = state.homeLat;
-
 
667
		day.stayLng = state.homeLng;
-
 
668
		day.stayName = state.homeName;
-
 
669
		drawReturnHome();
914
    drawRoute();
670
		startNextDay(state.homeLat, state.homeLng, state.homeName);
-
 
671
	} else {
-
 
672
		if (!state._hotelLat) {
-
 
673
			alert('Select hotel location');
-
 
674
			return;
-
 
675
		}
-
 
676
		day.stayLat = state._hotelLat;
-
 
677
		day.stayLng = state._hotelLng;
-
 
678
		day.stayName = state._hotelName;
-
 
679
		startNextDay(state._hotelLat, state._hotelLng, state._hotelName);
-
 
680
		state._hotelLat = null;
-
 
681
		state._hotelLng = null;
-
 
682
	}
-
 
683
	$('#modal-daybreak').removeClass('show');
-
 
684
	renderRoutePanel();
915
	renderRoutePanel();
-
 
916
    updateBottomBar();
685
});
917
});
686
 
918
 
687
$('#btn-finish').on('click', function () {
919
$('#btn-finish').on('click', function () {
688
	if (state.days.every(function (d) {
920
	if (state.days.every(function (d) {
689
		return d.visits.length === 0;
921
		return d.visits.length === 0;
690
	})) {
922
	})) {
691
		alert('No visits added');
923
		alert('No visits added');
692
		return;
924
		return;
693
	}
925
	}
694
 
926
 
695
	// Ask for beat title
-
 
696
	var beatTitle = prompt('Enter a name for this beat:', '');
927
    var beatTitle = state.beatTitle || 'Beat';
697
	if (beatTitle === null) return; // cancelled
-
 
698
	if (!beatTitle.trim()) {
-
 
699
		alert('Please enter a beat name');
-
 
700
		return;
-
 
701
	}
-
 
702
 
928
 
703
	// Build plan data and save to DB first
929
	// Build plan data and save to DB first
704
	var daysData = [];
930
	var daysData = [];
705
	state.days.forEach(function (day) {
931
	state.days.forEach(function (day) {
706
		if (day.visits.length === 0) return;
932
		if (day.visits.length === 0) return;
Line 738... Line 964...
738
			var result = data.response || data;
964
			var result = data.response || data;
739
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
965
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
740
 
966
 
741
			if (result.status) {
967
			if (result.status) {
742
				state.savedPlanGroupId = result.planGroupId;
968
				state.savedPlanGroupId = result.planGroupId;
743
				if (result.duplicate) {
-
 
744
					alert('This beat already exists (same visits). Switching to calendar.');
-
 
745
				}
-
 
746
 
-
 
747
				// Switch to calendar tab
-
 
748
				$('.panel-tab').removeClass('active');
-
 
749
				$('.panel-tab[data-tab="calendar"]').addClass('active');
-
 
750
				$('#panel-route').hide();
-
 
751
				$('#panel-calendar').show();
-
 
752
				$('#cal-grid-container').show();
-
 
753
				$('#bottom-bar').hide();
969
				$('#bottom-bar').hide();
754
 
970
 
-
 
971
                if (result.duplicate) {
-
 
972
                    alert('This beat already exists (same visits).');
755
				var now = new Date();
973
                } else {
756
				state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
974
                    alert('Beat "' + beatTitle + '" saved successfully!');
-
 
975
                }
-
 
976
 
-
 
977
                // Go back to beat list
757
				loadCalendarData();
978
                showBeatList();
758
			} else {
979
			} else {
759
				alert('Error saving beat plan');
980
				alert('Error saving beat plan');
760
			}
981
			}
761
		},
982
		},
762
		error: function (xhr) {
983
		error: function (xhr) {
Line 772... Line 993...
772
	$('.panel-tab').removeClass('active');
993
	$('.panel-tab').removeClass('active');
773
	$(this).addClass('active');
994
	$(this).addClass('active');
774
	$('#panel-route').toggle(tab === 'route');
995
	$('#panel-route').toggle(tab === 'route');
775
	$('#panel-calendar').toggle(tab === 'calendar');
996
	$('#panel-calendar').toggle(tab === 'calendar');
776
	$('#cal-grid-container').toggle(tab === 'calendar');
997
	$('#cal-grid-container').toggle(tab === 'calendar');
777
	$('#bottom-bar').toggle(tab === 'route' && state.days.length > 0);
998
    $('#bottom-bar').toggle(tab === 'route' && state.mode === 'create' && state.days && state.days.length > 0);
778
 
999
 
779
	if (tab === 'calendar' && !state.calMonth) {
1000
	if (tab === 'calendar' && !state.calMonth) {
780
		var now = new Date();
1001
		var now = new Date();
781
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
1002
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
782
		loadCalendarData();
1003
		loadCalendarData();
Line 839... Line 1060...
839
				beatDateMap[d.planDate].push({
1060
				beatDateMap[d.planDate].push({
840
					name: b.beatName,
1061
					name: b.beatName,
841
					color: b.beatColor,
1062
					color: b.beatColor,
842
					day: d.dayNumber,
1063
					day: d.dayNumber,
843
					status: b.status,
1064
					status: b.status,
844
					visits: d.visitCount
1065
                    visits: d.visitCount,
-
 
1066
                    planGroupId: b.planGroupId
845
				});
1067
				});
846
			}
1068
			}
847
		});
1069
		});
848
	});
1070
	});
849
 
1071
 
Line 863... Line 1085...
863
			var hol = !!holidayMap[ds];
1085
			var hol = !!holidayMap[ds];
864
			var isToday = ds === today;
1086
			var isToday = ds === today;
865
			var beats = beatDateMap[ds] || [];
1087
			var beats = beatDateMap[ds] || [];
866
			var isPicked = state.calPickedDates.indexOf(ds) > -1;
1088
			var isPicked = state.calPickedDates.indexOf(ds) > -1;
867
 
1089
 
-
 
1090
            var isPast = ds < today;
-
 
1091
            var hasBeats = beats.length > 0;
-
 
1092
 
868
			var cls = [];
1093
			var cls = [];
869
			if (sun) cls.push('blocked');
1094
            if (hol && !sun) cls.push('holiday', 'blocked');
870
			else if (hol) cls.push('holiday');
1095
            else if (isPast) cls.push('blocked', 'past');
-
 
1096
            if (sun && !isPast) cls.push('sunday'); // Sundays styled but not blocked
-
 
1097
            if (sun && isPast) cls.push('blocked', 'past');
871
			if (isToday) cls.push('today');
1098
			if (isToday) cls.push('today');
872
			if (!inMonth) cls.push('blocked');
1099
			if (!inMonth) cls.push('blocked');
873
 
1100
 
874
			var style = inMonth ? '' : 'opacity:0.3;';
1101
			var style = inMonth ? '' : 'opacity:0.3;';
-
 
1102
            if (isPast && inMonth) style += 'opacity:0.5;';
875
 
1103
 
876
			row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" style="' + style + '">';
1104
            row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" data-hasbeat="' + (hasBeats ? '1' : '0') + '" style="' + style + '">';
877
			row += '<div class="cal-date">' + d.getDate() + '</div>';
1105
			row += '<div class="cal-date">' + d.getDate() + '</div>';
878
			if (hol) row += '<div class="cal-holiday-label">' + holidayMap[ds] + '</div>';
1106
			if (hol) row += '<div class="cal-holiday-label">' + holidayMap[ds] + '</div>';
879
 
1107
 
880
			beats.forEach(function (b) {
1108
			beats.forEach(function (b) {
881
				var chipCls = 'cal-chip';
1109
				var chipCls = 'cal-chip';
882
				if (b.status === 'running') chipCls += ' running';
1110
				if (b.status === 'running') chipCls += ' running';
883
				if (b.status === 'completed') chipCls += ' completed';
1111
				if (b.status === 'completed') chipCls += ' completed';
-
 
1112
                var removeBtn = (!isPast && b.status !== 'running' && b.status !== 'completed')
-
 
1113
                    ? '<span class="cal-chip-remove" data-date="' + ds + '" data-plangroup="' + (b.planGroupId || '') + '" title="Remove">&times;</span>'
-
 
1114
                    : '';
-
 
1115
                row += '<span class="' + chipCls + '" style="background:' + b.color + ';position:relative;">'
884
				row += '<span class="' + chipCls + '" style="background:' + b.color + '">' + b.name + ' D' + b.day + (b.status === 'running' ? ' [live]' : '') + ' (' + b.visits + ')</span>';
1116
                    + b.name + ' D' + b.day + (b.status === 'running' ? ' [live]' : '') + ' (' + b.visits + ')'
-
 
1117
                    + removeBtn + '</span>';
885
			});
1118
			});
886
 
1119
 
887
			if (isPicked) {
1120
			if (isPicked) {
888
				var idx = state.calPickedDates.indexOf(ds) + 1;
1121
				var idx = state.calPickedDates.indexOf(ds) + 1;
889
				row += '<span class="cal-chip" style="background:#3b82f6;border:1px dashed #93c5fd;">Day ' + idx + '</span>';
1122
				row += '<span class="cal-chip" style="background:#3b82f6;border:1px dashed #93c5fd;">Day ' + idx + '</span>';
Line 902... Line 1135...
902
 
1135
 
903
function renderBeatCards(beats) {
1136
function renderBeatCards(beats) {
904
	var html = '';
1137
	var html = '';
905
	if (!beats || beats.length === 0) {
1138
	if (!beats || beats.length === 0) {
906
		html = '<p style="color:#64748b;font-size:12px;padding:20px;">No beats scheduled. Create a beat first.</p>';
1139
		html = '<p style="color:#64748b;font-size:12px;padding:20px;">No beats scheduled. Create a beat first.</p>';
-
 
1140
        $('#cal-beat-cards').html(html);
-
 
1141
        return;
907
	}
1142
	}
-
 
1143
 
-
 
1144
    // Deduplicate by beatName — group beats with same name, show count
-
 
1145
    var beatGroups = {};
908
	(beats || []).forEach(function (b) {
1146
	(beats || []).forEach(function (b) {
-
 
1147
        var key = b.beatName || b.planGroupId;
-
 
1148
        if (!beatGroups[key]) {
-
 
1149
            beatGroups[key] = {
-
 
1150
                beatName: b.beatName,
-
 
1151
                beatColor: b.beatColor,
-
 
1152
                instances: []
-
 
1153
            };
-
 
1154
        }
-
 
1155
        beatGroups[key].instances.push(b);
-
 
1156
    });
-
 
1157
 
-
 
1158
    Object.keys(beatGroups).forEach(function (key) {
-
 
1159
        var group = beatGroups[key];
-
 
1160
        var first = group.instances[0];
909
		var badge = b.status === 'running' ? '<span class="status-badge status-running">Live</span>' :
1161
        var scheduledCount = group.instances.filter(function (b) {
910
			b.status === 'completed' ? '<span class="status-badge status-completed">Done</span>' :
1162
            return b.status === 'scheduled';
-
 
1163
        }).length;
911
				b.status === 'unscheduled' ? '<span class="status-badge" style="background:#f59e0b;color:#0f172a;">Unscheduled</span>' :
1164
        var unscheduledCount = group.instances.filter(function (b) {
912
					'<span class="status-badge status-scheduled">Scheduled</span>';
1165
            return b.status === 'unscheduled';
-
 
1166
        }).length;
-
 
1167
        var totalInstances = group.instances.length;
913
 
1168
 
914
		var totalV = 0, totalKm = 0;
1169
		var totalV = 0, totalKm = 0;
915
		b.days.forEach(function (d) {
1170
        first.days.forEach(function (d) {
916
			totalV += d.visitCount || 0;
1171
            totalV += d.visitCount || 0;
917
			totalKm += d.totalKm || 0;
1172
            totalKm += d.totalKm || 0;
-
 
1173
        });
-
 
1174
 
-
 
1175
        // Use first unscheduled instance for scheduling, or first overall
-
 
1176
        var primaryPg = first.planGroupId;
-
 
1177
        var unscheduledInstance = group.instances.find(function (b) {
-
 
1178
            return b.status === 'unscheduled';
918
		});
1179
        });
-
 
1180
        if (unscheduledInstance) primaryPg = unscheduledInstance.planGroupId;
-
 
1181
 
-
 
1182
        var statusText = '';
-
 
1183
        if (scheduledCount > 0) statusText += scheduledCount + ' scheduled';
-
 
1184
        if (unscheduledCount > 0) statusText += (statusText ? ', ' : '') + unscheduledCount + ' unscheduled';
-
 
1185
 
-
 
1186
        var isScheduling = state.calSelectedBeat === primaryPg && state.calPickedDates.length > 0;
-
 
1187
 
-
 
1188
        html += '<div class="beat-card" data-plangroup="' + primaryPg + '" data-beatname="' + (group.beatName || '') + '" draggable="true">';
-
 
1189
        html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + group.beatColor + ';"></span> ' + group.beatName + '</div>';
-
 
1190
        html += '<div class="beat-meta">' + first.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
-
 
1191
        html += '<div class="beat-meta" style="color:#94a3b8;">' + statusText + ' (' + totalInstances + ' total)</div>';
-
 
1192
 
-
 
1193
        html += '<div class="beat-actions">';
-
 
1194
        if (isScheduling) {
-
 
1195
            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>';
-
 
1196
            html += '<button class="btn-sm btn-cancel-schedule" style="background:#475569;color:#e2e8f0;">Cancel</button>';
-
 
1197
        } else {
-
 
1198
            html += '<button class="btn-sm btn-schedule" data-pg="' + primaryPg + '" data-days="' + first.days.length + '">Schedule</button>';
-
 
1199
        }
-
 
1200
        html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" onclick="deleteBeat(\'' + primaryPg + '\')">Delete</button>';
-
 
1201
        html += '</div>';
-
 
1202
 
-
 
1203
        if (isScheduling) {
-
 
1204
            html += '<div style="font-size:10px;color:#60a5fa;margin-top:4px;">Dates: ' + state.calPickedDates.join(', ') + '</div>';
-
 
1205
            html += '<div style="font-size:10px;color:#94a3b8;">Click calendar cells to adjust</div>';
-
 
1206
        }
919
 
1207
 
920
		html += '<div class="beat-card" data-plangroup="' + b.planGroupId + '">';
-
 
921
		html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + b.beatColor + ';"></span> ' + b.beatName + ' ' + badge + '</div>';
-
 
922
		html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
-
 
923
		if (b.status !== 'running') {
-
 
924
			var isScheduling = state.calSelectedBeat === b.planGroupId && state.calPickedDates.length > 0;
-
 
925
			html += '<div class="beat-actions">';
-
 
926
			if (isScheduling) {
-
 
927
				html += '<button class="btn-sm btn-confirm-schedule" data-pg="' + b.planGroupId + '" data-days="' + b.days.length + '" style="background:#22c55e;color:#fff;">Confirm (' + state.calPickedDates.length + ' dates)</button>';
-
 
928
				html += '<button class="btn-sm btn-cancel-schedule" style="background:#475569;color:#e2e8f0;">Cancel</button>';
-
 
929
			} else {
-
 
930
				html += '<button class="btn-sm btn-schedule" data-pg="' + b.planGroupId + '" data-days="' + b.days.length + '">Schedule</button>';
-
 
931
				html += '<button class="btn-sm btn-repeat" data-pg="' + b.planGroupId + '" data-days="' + b.days.length + '">Repeat</button>';
-
 
932
			}
-
 
933
			html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" data-pg="' + b.planGroupId + '" onclick="deleteBeat(\'' + b.planGroupId + '\')">Delete</button>';
-
 
934
			html += '</div>';
-
 
935
			if (isScheduling) {
-
 
936
				html += '<div style="font-size:10px;color:#60a5fa;margin-top:4px;">Dates: ' + state.calPickedDates.join(', ') + '</div>';
1208
        html += '<div style="font-size:9px;color:#475569;margin-top:4px;">Drag onto calendar to schedule</div>';
937
				html += '<div style="font-size:10px;color:#94a3b8;">Click calendar cells to adjust</div>';
-
 
938
			}
-
 
939
		}
-
 
940
		html += '</div>';
1209
		html += '</div>';
941
	});
1210
	});
-
 
1211
 
942
	$('#cal-beat-cards').html(html);
1212
	$('#cal-beat-cards').html(html);
943
}
1213
}
944
 
1214
 
945
// Schedule button — suggest dates, show Confirm button in beat card
1215
// Schedule button — suggest dates, show Confirm button in beat card
946
$(document).on('click', '.btn-schedule', function (e) {
1216
$(document).on('click', '.btn-schedule', function (e) {
Line 963... Line 1233...
963
			}
1233
			}
964
		}
1234
		}
965
	});
1235
	});
966
});
1236
});
967
 
1237
 
968
// Click calendar cell to toggle picked date
1238
// Click calendar cell during Schedule flow — auto-fill consecutive days from clicked date
969
$(document).on('click', '#cal-grid-content td:not(.blocked)', function () {
1239
$(document).on('click', '#cal-grid-content td', function () {
970
	if (!state.calSelectedBeat) return;
1240
	if (!state.calSelectedBeat) return;
971
	var ds = $(this).data('date');
1241
	var ds = $(this).data('date');
972
	var idx = state.calPickedDates.indexOf(ds);
1242
    if (!ds || isDateBlocked(ds)) return;
-
 
1243
 
-
 
1244
    // Find the beat to get days needed
973
	if (idx > -1) state.calPickedDates.splice(idx, 1);
1245
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
-
 
1246
        return b.planGroupId === state.calSelectedBeat;
-
 
1247
    });
974
	else state.calPickedDates.push(ds);
1248
    var daysNeeded = beat ? beat.days.length : 1;
-
 
1249
 
-
 
1250
    // Auto-fill consecutive slots from clicked date
-
 
1251
    var slots = findConsecutiveSlots(ds, daysNeeded);
-
 
1252
    if (slots) {
975
	state.calPickedDates.sort();
1253
        state.calPickedDates = slots;
976
	renderCalGrid(state.calData);
1254
        renderCalGrid(state.calData);
977
	renderBeatCards(state.calData.scheduledBeats);
1255
        renderBeatCards(state.calData.scheduledBeats);
-
 
1256
    }
978
});
1257
});
979
 
1258
 
980
// Cancel scheduling mode
1259
// Cancel scheduling mode
981
$(document).on('click', '.btn-cancel-schedule', function (e) {
1260
$(document).on('click', '.btn-cancel-schedule', function (e) {
982
	e.stopPropagation();
1261
	e.stopPropagation();
Line 1019... Line 1298...
1019
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1298
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1020
		}
1299
		}
1021
	});
1300
	});
1022
});
1301
});
1023
 
1302
 
1024
// Repeat
1303
// ============ DRAG & DROP BEAT TO CALENDAR ============
1025
$(document).on('click', '.btn-repeat', function (e) {
1304
$(document).on('dragstart', '.beat-card', function (e) {
1026
	e.stopPropagation();
1305
    var pg = $(this).data('plangroup');
1027
	var pg = $(this).data('pg');
1306
    var beatName = $(this).data('beatname');
1028
	var days = parseInt($(this).data('days'));
1307
    e.originalEvent.dataTransfer.setData('text/plain', pg);
-
 
1308
    e.originalEvent.dataTransfer.setData('beatname', beatName || '');
-
 
1309
    $(this).css('opacity', '0.5');
-
 
1310
});
1029
 
1311
 
1030
	$.ajax({
-
 
1031
		url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
-
 
1032
		data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
-
 
1033
		success: function (r) {
-
 
1034
			var data = r.response || r;
-
 
1035
			if ((data.suggestedDates || []).length < days) {
1312
$(document).on('dragend', '.beat-card', function () {
1036
				alert('Not enough slots this month');
1313
    $(this).css('opacity', '1');
1037
				return;
-
 
1038
			}
1314
});
1039
			if (!confirm('Repeat beat on: ' + data.suggestedDates.join(', ') + '?')) return;
-
 
1040
 
1315
 
-
 
1316
// Calendar cells accept drops
-
 
1317
$(document).on('dragover', '#cal-grid-content td', function (e) {
-
 
1318
    var ds = $(this).data('date');
-
 
1319
    if (!ds) return;
-
 
1320
    if (isDateBlocked(ds)) return; // don't allow drop indicator on blocked dates
-
 
1321
    e.preventDefault();
-
 
1322
    $(this).css('background', '#1e3a5f');
-
 
1323
});
-
 
1324
 
-
 
1325
$(document).on('dragleave', '#cal-grid-content td', function () {
-
 
1326
    $(this).css('background', '');
-
 
1327
});
-
 
1328
 
-
 
1329
$(document).on('drop', '#cal-grid-content td', function (e) {
-
 
1330
    e.preventDefault();
-
 
1331
    $(this).css('background', '');
-
 
1332
 
-
 
1333
    var pg = e.originalEvent.dataTransfer.getData('text/plain');
-
 
1334
    var dropDate = $(this).data('date');
-
 
1335
    if (!pg || !dropDate) return;
-
 
1336
 
-
 
1337
    if (isDateBlocked(dropDate)) {
-
 
1338
        alert('Cannot schedule on this date (past, Sunday, or holiday)');
-
 
1339
        return;
-
 
1340
    }
-
 
1341
 
-
 
1342
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
-
 
1343
        return b.planGroupId === pg;
-
 
1344
    });
-
 
1345
    if (!beat) return;
-
 
1346
    var beatName = beat.beatName || 'Beat';
-
 
1347
    var daysNeeded = beat.days ? beat.days.length : 1;
-
 
1348
 
-
 
1349
    // Auto-fill consecutive available days starting from drop date
-
 
1350
    var scheduleDates = findConsecutiveSlots(dropDate, daysNeeded);
-
 
1351
 
-
 
1352
    if (!scheduleDates) return; // alerts already shown inside findConsecutiveSlots
-
 
1353
 
-
 
1354
    var dateStr = scheduleDates.map(function (d, i) {
-
 
1355
        return 'Day ' + (i + 1) + ': ' + d;
-
 
1356
    }).join('\n');
-
 
1357
    if (!confirm('Schedule "' + beatName + '" (' + daysNeeded + ' days):\n\n' + dateStr + '\n\nConfirm?')) return;
-
 
1358
 
1041
			$.ajax({
1359
    $.ajax({
1042
				url: context + "/beatPlan/repeatBeat", type: "POST",
1360
        url: context + "/beatPlan/repeatBeat", type: "POST",
-
 
1361
        data: {
-
 
1362
            sourcePlanGroupId: pg,
-
 
1363
            authUserId: state.authUserId,
1043
				data: {sourcePlanGroupId: pg, authUserId: state.authUserId, dates: JSON.stringify(data.suggestedDates)},
1364
            dates: JSON.stringify(scheduleDates)
-
 
1365
        },
1044
				success: function () {
1366
        success: function () {
1045
					loadCalendarData();
1367
            loadCalendarData();
-
 
1368
        },
1046
					alert('Beat repeated!');
1369
        error: function (xhr) {
-
 
1370
            alert('Error: ' + (xhr.responseText || xhr.statusText));
-
 
1371
        }
1047
				}
1372
    });
1048
			});
1373
});
-
 
1374
 
-
 
1375
// Find N consecutive available days starting from startDate, skipping Sundays/holidays
-
 
1376
// Returns array of date strings, or null if can't fit
-
 
1377
function findConsecutiveSlots(startDateStr, daysNeeded) {
-
 
1378
    var startDate = new Date(startDateStr);
-
 
1379
    var startMonth = startDate.getMonth();
-
 
1380
    var dates = [];
-
 
1381
    var d = new Date(startDate);
-
 
1382
    var sundayAsked = false;
-
 
1383
    var includeSundays = false;
-
 
1384
 
-
 
1385
    while (dates.length < daysNeeded) {
-
 
1386
        var ds = fmtDate(d);
-
 
1387
 
-
 
1388
        // Rule 3: must stay within same month
-
 
1389
        if (d.getMonth() !== startMonth) {
-
 
1390
            alert('Beat has ' + daysNeeded + ' days but only ' + dates.length + ' available days left in this month starting from ' + startDateStr + '. Try an earlier date.');
-
 
1391
            return null;
1049
		}
1392
		}
-
 
1393
 
-
 
1394
        // Past dates — skip
-
 
1395
        var today = fmtDate(new Date());
-
 
1396
        if (ds <= today) {
-
 
1397
            d.setDate(d.getDate() + 1);
-
 
1398
            continue;
-
 
1399
        }
-
 
1400
 
-
 
1401
        // Holidays — always skip
-
 
1402
        if (isHoliday(ds)) {
-
 
1403
            d.setDate(d.getDate() + 1);
-
 
1404
            continue;
-
 
1405
        }
-
 
1406
 
-
 
1407
        // Sunday — ask once whether to include
-
 
1408
        if (isSunday(ds)) {
-
 
1409
            if (!sundayAsked) {
-
 
1410
                sundayAsked = true;
-
 
1411
                includeSundays = confirm('This beat falls on a Sunday (' + ds + '). Include Sundays in the schedule?');
-
 
1412
            }
-
 
1413
            if (!includeSundays) {
-
 
1414
                d.setDate(d.getDate() + 1);
-
 
1415
                continue;
-
 
1416
            }
-
 
1417
        }
-
 
1418
 
-
 
1419
        // Already occupied — block
-
 
1420
        if (getBeatsOnDate(ds).length > 0) {
-
 
1421
            alert('Cannot schedule: ' + ds + ' already has a beat scheduled. Consecutive days not available from ' + startDateStr + '.');
-
 
1422
            return null;
-
 
1423
        }
-
 
1424
 
-
 
1425
        dates.push(ds);
-
 
1426
        d.setDate(d.getDate() + 1);
-
 
1427
    }
-
 
1428
 
-
 
1429
    return dates;
-
 
1430
}
-
 
1431
 
-
 
1432
function isDateBlocked(ds) {
-
 
1433
    var today = fmtDate(new Date());
-
 
1434
    if (ds <= today) return true; // past or today
-
 
1435
    // Sundays NOT blocked — handled via confirm dialog instead
-
 
1436
    // Only block holidays
-
 
1437
    if (state.calData && state.calData.blockedDates) {
-
 
1438
        var blocked = state.calData.blockedDates;
-
 
1439
        // blockedDates from server includes Sundays+holidays, but we only block holidays now
-
 
1440
        var d = new Date(ds);
-
 
1441
        if (d.getDay() !== 0) { // not Sunday — check holidays
-
 
1442
            if (Array.isArray(blocked) && blocked.indexOf(ds) > -1) return true;
-
 
1443
            if (typeof blocked === 'object' && blocked[ds]) return true;
-
 
1444
        }
-
 
1445
    }
-
 
1446
    return false;
-
 
1447
}
-
 
1448
 
-
 
1449
function isSunday(ds) {
-
 
1450
    return new Date(ds).getDay() === 0;
-
 
1451
}
-
 
1452
 
-
 
1453
function isHoliday(ds) {
-
 
1454
    if (!state.calData || !state.calData.holidays) return false;
-
 
1455
    return state.calData.holidays.some(function (h) {
-
 
1456
        return h.date === ds;
-
 
1457
    });
-
 
1458
}
-
 
1459
 
-
 
1460
function getBeatsOnDate(ds) {
-
 
1461
    var result = [];
-
 
1462
    if (!state.calData || !state.calData.scheduledBeats) return result;
-
 
1463
    state.calData.scheduledBeats.forEach(function (b) {
-
 
1464
        (b.days || []).forEach(function (d) {
-
 
1465
            if (d.planDate === ds) result.push({planGroupId: b.planGroupId, beatName: b.beatName});
-
 
1466
        });
-
 
1467
    });
-
 
1468
    return result;
-
 
1469
}
-
 
1470
 
-
 
1471
// ============ REMOVE BEAT FROM CALENDAR DATE ============
-
 
1472
$(document).on('click', '.cal-chip-remove', function (e) {
-
 
1473
    e.stopPropagation();
-
 
1474
    var date = $(this).data('date');
-
 
1475
    var planGroupId = $(this).data('plangroup');
-
 
1476
    if (!planGroupId || !date) return;
-
 
1477
 
-
 
1478
    if (!confirm('Remove this beat from ' + date + '?')) return;
-
 
1479
 
-
 
1480
    // Delete the entire beat instance (planGroupId)
-
 
1481
    $.ajax({
-
 
1482
        url: context + "/beatPlan/delete", type: "POST",
-
 
1483
        data: {planGroupId: planGroupId},
-
 
1484
        success: function () {
-
 
1485
            loadCalendarData();
-
 
1486
        },
-
 
1487
        error: function (xhr) {
-
 
1488
            alert('Error: ' + (xhr.responseText || xhr.statusText));
-
 
1489
        }
1050
	});
1490
	});
1051
});
1491
});
1052
 
1492
 
1053
// ============ DELETE BEAT ============
1493
// ============ DELETE BEAT ============
1054
function deleteBeat(planGroupId) {
1494
function deleteBeat(planGroupId) {