Subversion Repositories SmartDukaan

Rev

Rev 36632 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

// ============================================================
// BEAT PLAN APP — Map-First Route Planner
// ============================================================

var map, infoWindow, homeMarker, previewLine;
var markers = {};
var routeLines = [];
var distLabels = [];

var state = {
        authUserId: 0,
        categoryId: 4,
        homeLat: null, homeLng: null, homeName: '',
        currentDay: 1,
        days: [], // [{dayNumber, startLat, startLng, startName, visits:[], endAction, stayName, stayLat, stayLng, totalKm, totalMins}]
        partners: [],
        calMonth: null,
        calData: null,
        calSelectedBeat: null,
        calPickedDates: []
};

var VISIT_MINS = 30;
var AVG_SPEED = 30;
var DAY_LIMIT = 540;
var ROAD_FACTOR = 1.3;
var COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ec4899', '#334155', '#14b8a6', '#dc2626'];

// ============ TOP BAR ============
$('#bp-level').on('change', function () {
        var level = $(this).val();
        $('#bp-auth-user').html('<option value="">Select User</option>');
        if (!level) return;

        $.ajax({
                url: context + "/beatPlan/getAuthUsers", type: "GET", dataType: "json",
                data: {categoryId: $('#bp-category').val(), escalationType: level},
                success: function (r) {
                        var data = r.response || r;
                        var html = '<option value="">Select User</option>';
                        data.forEach(function (u) {
                                html += '<option value="' + u.id + '">' + u.name + '</option>';
                        });
                        $('#bp-auth-user').html(html);
                }
        });
});

$('#bp-load').on('click', function () {
        var uid = $('#bp-auth-user').val();
        if (!uid) {
                alert('Select a user');
                return;
        }
        state.authUserId = parseInt(uid);
        state.categoryId = parseInt($('#bp-category').val());
        $('#bp-user-label').text($('#bp-auth-user option:selected').text());

        // Check home location
        $.ajax({
                url: context + "/beatPlan/getBaseLocation", type: "GET", dataType: "json",
                data: {authUserId: uid},
                success: function (r) {
                        var data = r.response || r;
                        if (data.locationName) {
                                state.homeLat = parseFloat(data.latitude);
                                state.homeLng = parseFloat(data.longitude);
                                state.homeName = data.locationName;
                                loadPartners();
                        } else {
                                showHomeModal();
                        }
                }
        });
});

// ============ HOME LOCATION MODAL ============
var homeMap, homeMapMarker;

function showHomeModal() {
        $('#modal-home').addClass('show');
        setTimeout(function () {
                if (!homeMap) {
                        homeMap = new google.maps.Map(document.getElementById('home-map'), {
                                center: {lat: 28.6, lng: 77.2},
                                zoom: 6
                        });
                        var ac = new google.maps.places.Autocomplete(document.getElementById('home-search'), {componentRestrictions: {country: 'in'}});
                        homeMapMarker = new google.maps.Marker({map: homeMap, draggable: true});

                        ac.addListener('place_changed', function () {
                                var p = ac.getPlace();
                                if (!p.geometry) return;
                                homeMap.setCenter(p.geometry.location);
                                homeMap.setZoom(14);
                                homeMapMarker.setPosition(p.geometry.location);
                                state.homeLat = p.geometry.location.lat();
                                state.homeLng = p.geometry.location.lng();
                                state.homeName = p.name || p.formatted_address || '';
                        });
                        homeMapMarker.addListener('dragend', function () {
                                state.homeLat = homeMapMarker.getPosition().lat();
                                state.homeLng = homeMapMarker.getPosition().lng();
                        });
                        homeMap.addListener('click', function (e) {
                                homeMapMarker.setPosition(e.latLng);
                                state.homeLat = e.latLng.lat();
                                state.homeLng = e.latLng.lng();
                        });
                }
        }, 200);
}

$('#home-cancel').on('click', function () {
        $('#modal-home').removeClass('show');
});
$('#home-confirm').on('click', function () {
        if (!state.homeLat) {
                alert('Please select a location');
                return;
        }
        $('#modal-home').removeClass('show');
        $.ajax({
                url: context + "/beatPlan/saveBaseLocation", type: "POST",
                data: {
                        authUserId: state.authUserId,
                        locationName: state.homeName || 'Home',
                        latitude: state.homeLat,
                        longitude: state.homeLng,
                        address: $('#home-search').val()
                }
        });
        loadPartners();
});

// ============ LOAD PARTNERS + INIT MAP ============
function loadPartners() {
        $.ajax({
                url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
                data: {
                        authUserId: state.authUserId,
                        categoryId: state.categoryId,
                        startLat: state.homeLat,
                        startLng: state.homeLng
                },
                success: function (r) {
                        var data = r.response || r;
                        state.partners = data.partners || [];
                        initDay1();
                        initMap();
                }
        });
}

function initDay1() {
        state.days = [{
                dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
                visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
        }];
        state.currentDay = 1;
        $('#bottom-bar').show();
        renderRoutePanel();
}

function initMap() {
        var center = {lat: state.homeLat, lng: state.homeLng};
        map = new google.maps.Map(document.getElementById('beat-map'), {
                center: center, zoom: 9,
                styles: [
                        {elementType: 'geometry', stylers: [{color: '#1d2c4d'}]},
                        {elementType: 'labels.text.fill', stylers: [{color: '#8ec3b9'}]},
                        {elementType: 'labels.text.stroke', stylers: [{color: '#1a3646'}]},
                        {featureType: 'road', elementType: 'geometry', stylers: [{color: '#304a7d'}]},
                        {featureType: 'water', elementType: 'geometry', stylers: [{color: '#0e1626'}]}
                ]
        });
        infoWindow = new google.maps.InfoWindow();

        // Home marker
        homeMarker = new google.maps.Marker({
                map: map, position: center, title: 'Home: ' + state.homeName,
                icon: {
                        url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
                        scaledSize: new google.maps.Size(32, 32)
                },
                zIndex: 100
        });

        // Partner markers with name labels
        for (var key in markers) {
                markers[key].setMap(null);
        }
        markers = {};

        state.partners.forEach(function (p) {
                if (!p.latitude || !p.longitude) return;
                var lat = parseFloat(p.latitude), lng = parseFloat(p.longitude);
                if (isNaN(lat) || isNaN(lng)) return;

                var m = new google.maps.Marker({
                        map: map, position: {lat: lat, lng: lng},
                        title: p.code + ' - ' + (p.outletName || p.businessName || ''),
                        label: {text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'},
                        icon: {
                                path: google.maps.SymbolPath.CIRCLE,
                                scale: 14,
                                fillColor: '#ef4444',
                                fillOpacity: 0.9,
                                strokeColor: '#fff',
                                strokeWeight: 1
                        }
                });

                m.addListener('click', function () {
                        showMarkerPopup(p, m);
                });
                m.addListener('mouseover', function () {
                        showPreviewLine(p);
                });
                m.addListener('mouseout', function () {
                        hidePreviewLine();
                });

                markers[p.fofoId] = m;
        });

        fitBounds();
}

// ============ HOVER PREVIEW ============
function showPreviewLine(partner) {
        if (!partner.latitude || !partner.longitude) return;
        var day = curDay();
        var lastPt = day.visits.length > 0
                ? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
                : {lat: day.startLat, lng: day.startLng};
        if (!lastPt.lat) return;

        var endPt = {lat: parseFloat(partner.latitude), lng: parseFloat(partner.longitude)};
        if (previewLine) previewLine.setMap(null);
        previewLine = new google.maps.Polyline({
                path: [lastPt, endPt],
                geodesic: true,
                strokeColor: '#60a5fa',
                strokeOpacity: 0.5,
                strokeWeight: 2,
                strokePattern: [10, 5],
                icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
                map: map
        });
}

function hidePreviewLine() {
        if (previewLine) {
                previewLine.setMap(null);
                previewLine = null;
        }
}

// ============ MARKER CLICK POPUP ============
function showMarkerPopup(partner, marker) {
        var day = curDay();
        var alreadyAdded = day.visits.some(function (v) {
                return v.id === partner.fofoId;
        });
        if (alreadyAdded) {
                infoWindow.setContent('<div class="marker-popup"><h4>' + partner.code + '</h4><p>Already in today\'s route</p></div>');
                infoWindow.open(map, marker);
                return;
        }

        var lastPt = day.visits.length > 0
                ? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
                : {lat: day.startLat, lng: day.startLng};

        var dist = 0, travelMins = 0;
        if (lastPt.lat && partner.latitude) {
                dist = haversine(lastPt.lat, lastPt.lng, parseFloat(partner.latitude), parseFloat(partner.longitude)) * ROAD_FACTOR;
                travelMins = (dist / AVG_SPEED) * 60;
        }

        var name = partner.outletName || partner.businessName || '';
        var addr = partner.address || '';

        var html = '<div class="marker-popup">';
        html += '<h4>' + partner.code + ' - ' + name + '</h4>';
        html += '<div class="popup-meta">' + addr + '</div>';
        html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + 'min visit</div>';
        html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
        html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
        html += '<button class="popup-nextday" onclick="addVisit(' + partner.fofoId + ',\'nextday\')">Continue Next Day</button>';
        html += '</div>';

        infoWindow.setContent(html);
        infoWindow.open(map, marker);
}

// ============ ADD VISIT ============
function addVisit(fofoId, action) {
        infoWindow.close();
        var p = state.partners.find(function (x) {
                return x.fofoId === fofoId;
        });
        if (!p) return;

        var day = curDay();
        var visit = {
                id: p.fofoId, type: 'partner',
                name: p.code + ' - ' + (p.outletName || p.businessName || ''),
                code: p.code,
                lat: p.latitude ? parseFloat(p.latitude) : null,
                lng: p.longitude ? parseFloat(p.longitude) : null
        };

        day.visits.push(visit);

        // Update marker to green
        if (markers[fofoId]) {
                markers[fofoId].setIcon({
                        path: google.maps.SymbolPath.CIRCLE, scale: 14,
                        fillColor: '#22c55e', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 2
                });
                markers[fofoId].setLabel({text: '' + day.visits.length, color: '#fff', fontSize: '11px', fontWeight: '700'});
        }

        recalcDay();
        drawRoute();
        renderRoutePanel();

        if (action === 'last') {
                // Last visit — end day, go home
                day.endAction = 'HOME';
                day.stayLat = state.homeLat;
                day.stayLng = state.homeLng;
                day.stayName = state.homeName;
                drawReturnHome();
                renderRoutePanel();
        } else if (action === 'nextday') {
                // Continue next day — end current day, start new day from here
                day.endAction = 'CONTINUE';
                day.stayLat = visit.lat;
                day.stayLng = visit.lng;
                day.stayName = visit.name;
                startNextDay(visit.lat, visit.lng, visit.name);
        }
        // 'next' — just added, continue on same day
}

function drawReturnHome() {
        var day = curDay();
        if (day.visits.length === 0) return;
        var lastVisit = day.visits[day.visits.length - 1];
        if (!lastVisit.lat || !state.homeLat) return;

        var line = new google.maps.Polyline({
                path: [{lat: lastVisit.lat, lng: lastVisit.lng}, {lat: state.homeLat, lng: state.homeLng}],
                geodesic: true, strokeColor: '#22c55e', strokeOpacity: 0.6, strokeWeight: 2,
                icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
                map: map
        });
        routeLines.push(line);

        // Add return distance to day total
        var retDist = haversine(lastVisit.lat, lastVisit.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
        day.totalKm += retDist;
        day.totalMins += (retDist / AVG_SPEED) * 60;
        updateBottomBar();
}

function startNextDay(startLat, startLng, startName) {
        var nextNum = state.days.length + 1;
        state.days.push({
                dayNumber: nextNum, startLat: startLat, startLng: startLng, startName: startName,
                visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
        });
        state.currentDay = nextNum;
        updateBottomBar();
        renderRoutePanel();
        // Reset marker colors for unvisited
        resetMarkerColors();
}

function resetMarkerColors() {
        var allVisited = {};
        state.days.forEach(function (d) {
                d.visits.forEach(function (v) {
                        allVisited[v.id] = true;
                });
        });

        state.partners.forEach(function (p) {
                if (!markers[p.fofoId]) return;
                if (allVisited[p.fofoId]) {
                        markers[p.fofoId].setIcon({
                                path: google.maps.SymbolPath.CIRCLE,
                                scale: 14,
                                fillColor: '#22c55e',
                                fillOpacity: 0.9,
                                strokeColor: '#fff',
                                strokeWeight: 1
                        });
                } else {
                        markers[p.fofoId].setIcon({
                                path: google.maps.SymbolPath.CIRCLE,
                                scale: 14,
                                fillColor: '#ef4444',
                                fillOpacity: 0.9,
                                strokeColor: '#fff',
                                strokeWeight: 1
                        });
                        markers[p.fofoId].setLabel({text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
                }
        });

        // Re-label current day visits
        var day = curDay();
        day.visits.forEach(function (v, i) {
                if (markers[v.id]) markers[v.id].setLabel({
                        text: '' + (i + 1),
                        color: '#fff',
                        fontSize: '11px',
                        fontWeight: '700'
                });
        });
}

// ============ ROUTE DRAWING ============
function clearRoute() {
        routeLines.forEach(function (l) {
                l.setMap(null);
        });
        routeLines = [];
        distLabels.forEach(function (l) {
                l.setMap(null);
        });
        distLabels = [];
}

function drawRoute() {
        clearRoute();
        var day = curDay();
        var pts = [{lat: day.startLat, lng: day.startLng}];
        day.visits.forEach(function (v) {
                if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
        });

        for (var i = 0; i < pts.length - 1; i++) {
                var line = new google.maps.Polyline({
                        path: [pts[i], pts[i + 1]],
                        geodesic: true,
                        strokeColor: '#3b82f6',
                        strokeOpacity: 0.9,
                        strokeWeight: 3,
                        map: map
                });
                routeLines.push(line);

                var d = haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
                var mid = {lat: (pts[i].lat + pts[i + 1].lat) / 2, lng: (pts[i].lng + pts[i + 1].lng) / 2};
                var lbl = new google.maps.Marker({
                        position: mid, map: map, icon: {path: google.maps.SymbolPath.CIRCLE, scale: 0},
                        label: {text: d.toFixed(1) + 'km', color: '#93c5fd', fontSize: '10px', fontWeight: '600'}
                });
                distLabels.push(lbl);
        }
}

function recalcDay() {
        var day = curDay();
        var pts = [{lat: day.startLat, lng: day.startLng}];
        day.visits.forEach(function (v) {
                if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
        });

        var km = 0;
        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;
        day.totalKm = km;
        day.totalMins = (km / AVG_SPEED) * 60 + day.visits.length * VISIT_MINS;
        updateBottomBar();
}

function updateBottomBar() {
        var day = curDay();
        $('#bb-day-label').text('Day ' + day.dayNumber);
        $('#bb-stops').text(day.visits.length + ' stops');
        $('#bb-distance').text(day.totalKm.toFixed(1) + ' km');
        $('#bb-time').text(fmtMins(day.totalMins));

        var pct = Math.min((day.totalMins / DAY_LIMIT) * 100, 100);
        var col = pct < 78 ? '#22c55e' : pct < 89 ? '#f59e0b' : '#ef4444';
        $('#bb-progress').css({width: pct + '%', background: col});
}

// ============ ROUTE PANEL ============
function renderRoutePanel() {
        var html = '';
        state.days.forEach(function (day) {
                var isCurrent = day.dayNumber === state.currentDay;
                html += '<div class="route-day">';
                html += '<div class="route-day-header">';
                html += '<span>Day ' + day.dayNumber + (isCurrent ? ' (current)' : '') + '</span>';
                html += '<span>' + fmtMins(day.totalMins) + '</span>';
                html += '</div>';

                // Start point
                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>';

                // Visits with distance between each
                var prevLat = day.startLat, prevLng = day.startLng;
                day.visits.forEach(function (v, i) {
                        // Distance connector from previous point
                        var segDist = 0, segTime = 0;
                        if (prevLat && prevLng && v.lat && v.lng) {
                                segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
                                segTime = (segDist / AVG_SPEED) * 60;
                        }
                        if (segDist > 0) {
                                html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">' + segDist.toFixed(1) + ' km | ' + fmtMins(segTime) + '</span></div>';
                        }

                        html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;">';
                        html += '<span class="stop-num">' + (i + 1) + '</span>';
                        html += '<div class="stop-info"><div class="stop-name">' + (v.code || '') + '</div>';
                        html += '<div class="stop-meta">' + (v.name || '') + '</div></div>';
                        html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">&times;</span>';
                        html += '</div>';

                        prevLat = v.lat;
                        prevLng = v.lng;
                });

                // Return home distance
                if (day.endAction === 'HOME' && day.visits.length > 0) {
                        var lastV = day.visits[day.visits.length - 1];
                        var retDist = 0, retTime = 0;
                        if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
                                retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
                                retTime = (retDist / AVG_SPEED) * 60;
                        }
                        if (retDist > 0) {
                                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>';
                        }
                        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>';
                }

                html += '<div class="day-summary">' + day.visits.length + ' stops | ' + day.totalKm.toFixed(1) + ' km</div>';
                html += '</div>';
        });

        $('#route-list').html(html);
}

// Click on stop name in route panel → show popup on map
$(document).on('click', '.route-stop-clickable', function (e) {
        if ($(e.target).hasClass('stop-remove')) return; // let remove handler handle it
        var fofoId = parseInt($(this).data('fofoid'));
        if (!fofoId || !markers[fofoId]) return;

        var marker = markers[fofoId];
        var p = state.partners.find(function (x) {
                return x.fofoId === fofoId;
        });
        if (!p) return;

        map.panTo(marker.getPosition());
        map.setZoom(12);

        var name = p.outletName || p.businessName || '';
        var addr = p.address || '';
        infoWindow.setContent('<div class="marker-popup"><h4>' + p.code + ' - ' + name + '</h4><div class="popup-meta">' + addr + '</div></div>');
        infoWindow.open(map, marker);
});

// Remove stop from route panel
$(document).on('click', '.stop-remove', function (e) {
        e.stopPropagation();
        var fofoId = parseInt($(this).data('fofoid'));
        var dayNum = parseInt($(this).data('daynum'));

        var day = state.days[dayNum - 1];
        if (!day) return;

        // Remove visit from day
        day.visits = day.visits.filter(function (v) {
                return v.id !== fofoId;
        });

        // Reset marker back to red (unvisited)
        if (markers[fofoId]) {
                var p = state.partners.find(function (x) {
                        return x.fofoId === fofoId;
                });
                markers[fofoId].setIcon({
                        path: google.maps.SymbolPath.CIRCLE, scale: 14,
                        fillColor: '#ef4444', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 1
                });
                markers[fofoId].setLabel({text: (p ? p.code : '') || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
        }

        // Re-number remaining markers for this day
        day.visits.forEach(function (v, i) {
                if (markers[v.id]) {
                        markers[v.id].setLabel({text: '' + (i + 1), color: '#fff', fontSize: '11px', fontWeight: '700'});
                }
        });

        recalcDay();
        drawRoute();
        renderRoutePanel();
        updateBottomBar();
});

// ============ END DAY / FINISH ============
$('#btn-end-day').on('click', function () {
        var day = curDay();
        if (day.visits.length === 0) {
                alert('Add at least one stop');
                return;
        }

        $('#daybreak-info').text('Day ' + day.dayNumber + ': ' + day.visits.length + ' stops, ' + day.totalKm.toFixed(1) + ' km, ' + fmtMins(day.totalMins));
        $('#modal-daybreak').addClass('show');
});

$('input[name="stay"]').on('change', function () {
        $('#hotel-fields').toggle($(this).val() === 'HOTEL');
        if ($(this).val() === 'HOTEL') {
                setTimeout(function () {
                        if (!window._hotelMap) {
                                window._hotelMap = new google.maps.Map(document.getElementById('hotel-map'), {
                                        center: {
                                                lat: 28.6,
                                                lng: 77.2
                                        }, zoom: 6
                                });
                                var hm = new google.maps.Marker({map: window._hotelMap, draggable: true});
                                var hac = new google.maps.places.Autocomplete(document.getElementById('hotel-search'), {componentRestrictions: {country: 'in'}});
                                hac.addListener('place_changed', function () {
                                        var p = hac.getPlace();
                                        if (!p.geometry) return;
                                        window._hotelMap.setCenter(p.geometry.location);
                                        window._hotelMap.setZoom(14);
                                        hm.setPosition(p.geometry.location);
                                        state._hotelLat = p.geometry.location.lat();
                                        state._hotelLng = p.geometry.location.lng();
                                        state._hotelName = p.name || p.formatted_address;
                                });
                                hm.addListener('dragend', function () {
                                        state._hotelLat = hm.getPosition().lat();
                                        state._hotelLng = hm.getPosition().lng();
                                });
                        }
                }, 200);
        }
});

$('#daybreak-cancel').on('click', function () {
        $('#modal-daybreak').removeClass('show');
});
$('#daybreak-confirm').on('click', function () {
        var day = curDay();
        var opt = $('input[name="stay"]:checked').val();
        day.endAction = opt;

        if (opt === 'HOME') {
                day.stayLat = state.homeLat;
                day.stayLng = state.homeLng;
                day.stayName = state.homeName;
                drawReturnHome();
                startNextDay(state.homeLat, state.homeLng, state.homeName);
        } else {
                if (!state._hotelLat) {
                        alert('Select hotel location');
                        return;
                }
                day.stayLat = state._hotelLat;
                day.stayLng = state._hotelLng;
                day.stayName = state._hotelName;
                startNextDay(state._hotelLat, state._hotelLng, state._hotelName);
                state._hotelLat = null;
                state._hotelLng = null;
        }
        $('#modal-daybreak').removeClass('show');
        renderRoutePanel();
});

$('#btn-finish').on('click', function () {
        if (state.days.every(function (d) {
                return d.visits.length === 0;
        })) {
                alert('No visits added');
                return;
        }

        // Ask for beat title
        var beatTitle = prompt('Enter a name for this beat:', '');
        if (beatTitle === null) return; // cancelled
        if (!beatTitle.trim()) {
                alert('Please enter a beat name');
                return;
        }

        // Build plan data and save to DB first
        var daysData = [];
        state.days.forEach(function (day) {
                if (day.visits.length === 0) return;
                daysData.push({
                        dayNumber: day.dayNumber,
                        startLocationName: day.startName,
                        startLatitude: day.startLat ? day.startLat.toString() : null,
                        startLongitude: day.startLng ? day.startLng.toString() : null,
                        endAction: day.endAction,
                        stayLocationName: day.stayName,
                        stayLatitude: day.stayLat ? day.stayLat.toString() : null,
                        stayLongitude: day.stayLng ? day.stayLng.toString() : null,
                        totalDistanceKm: day.totalKm,
                        totalTimeMins: Math.round(day.totalMins),
                        visits: day.visits.map(function (v) {
                                return {id: v.id, type: v.type || 'partner'};
                        })
                });
        });

        // Dates will be assigned later via calendar — pass nulls for now
        var dates = daysData.map(function () {
                return null;
        });
        var planData = JSON.stringify({days: daysData, dates: dates, beatName: beatTitle.trim()});

        $('#btn-finish').prop('disabled', true).text('Saving...');

        $.ajax({
                url: context + "/beatPlan/submitPlan",
                type: "POST",
                data: {authUserId: state.authUserId, planData: planData},
                success: function (response) {
                        var data = (typeof response === 'string' ? JSON.parse(response) : response);
                        var result = data.response || data;
                        $('#btn-finish').prop('disabled', false).text('Finish Plan');

                        if (result.status) {
                                state.savedPlanGroupId = result.planGroupId;
                                if (result.duplicate) {
                                        alert('This beat already exists (same visits). Switching to calendar.');
                                }

                                // Switch to calendar tab
                                $('.panel-tab').removeClass('active');
                                $('.panel-tab[data-tab="calendar"]').addClass('active');
                                $('#panel-route').hide();
                                $('#panel-calendar').show();
                                $('#cal-grid-container').show();
                                $('#bottom-bar').hide();

                                var now = new Date();
                                state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
                                loadCalendarData();
                        } else {
                                alert('Error saving beat plan');
                        }
                },
                error: function (xhr) {
                        $('#btn-finish').prop('disabled', false).text('Finish Plan');
                        alert('Error: ' + (xhr.responseText || xhr.statusText));
                }
        });
});

// ============ PANEL TABS ============
$('.panel-tab').on('click', function () {
        var tab = $(this).data('tab');
        $('.panel-tab').removeClass('active');
        $(this).addClass('active');
        $('#panel-route').toggle(tab === 'route');
        $('#panel-calendar').toggle(tab === 'calendar');
        $('#cal-grid-container').toggle(tab === 'calendar');
        $('#bottom-bar').toggle(tab === 'route' && state.days.length > 0);

        if (tab === 'calendar' && !state.calMonth) {
                var now = new Date();
                state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
                loadCalendarData();
        }
});

// ============ CALENDAR ============
$('#cal-prev').on('click', function () {
        navMonth(-1);
});
$('#cal-next').on('click', function () {
        navMonth(1);
});

function navMonth(delta) {
        var p = state.calMonth.split('-');
        var d = new Date(parseInt(p[0]), parseInt(p[1]) - 1 + delta, 1);
        state.calMonth = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
        loadCalendarData();
}

function loadCalendarData() {
        if (!state.authUserId) return;
        $('#cal-month-label').text(state.calMonth);

        $.ajax({
                url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
                data: {authUserId: state.authUserId, month: state.calMonth},
                success: function (r) {
                        var data = r.response || r;
                        state.calData = data;
                        renderCalGrid(data);
                        renderBeatCards(data.scheduledBeats);
                }
        });
}

function renderCalGrid(data) {
        var p = state.calMonth.split('-');
        var year = parseInt(p[0]), month = parseInt(p[1]) - 1;
        var first = new Date(year, month, 1);
        var last = new Date(year, month + 1, 0);
        var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
        $('#cal-month-label').text(months[month] + ' ' + year);

        var holidayMap = {};
        (data.holidays || []).forEach(function (h) {
                holidayMap[h.date] = h.occasion;
        });
        var blockedSet = {};
        (data.blockedDates || []).forEach(function (d) {
                blockedSet[d] = true;
        });

        var beatDateMap = {};
        (data.scheduledBeats || []).forEach(function (b) {
                (b.days || []).forEach(function (d) {
                        if (d.planDate) {
                                if (!beatDateMap[d.planDate]) beatDateMap[d.planDate] = [];
                                beatDateMap[d.planDate].push({
                                        name: b.beatName,
                                        color: b.beatColor,
                                        day: d.dayNumber,
                                        status: b.status,
                                        visits: d.visitCount
                                });
                        }
                });
        });

        var today = fmtDate(new Date());
        var startOff = (first.getDay() + 6) % 7;
        var calStart = new Date(year, month, 1 - startOff);

        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>';
        var d = new Date(calStart);
        for (var w = 0; w < 6; w++) {
                var hasDay = false;
                var row = '<tr>';
                for (var wd = 0; wd < 7; wd++) {
                        var ds = fmtDate(d);
                        var inMonth = d.getMonth() === month;
                        var sun = d.getDay() === 0;
                        var hol = !!holidayMap[ds];
                        var isToday = ds === today;
                        var beats = beatDateMap[ds] || [];
                        var isPicked = state.calPickedDates.indexOf(ds) > -1;

                        var cls = [];
                        if (sun) cls.push('blocked');
                        else if (hol) cls.push('holiday');
                        if (isToday) cls.push('today');
                        if (!inMonth) cls.push('blocked');

                        var style = inMonth ? '' : 'opacity:0.3;';

                        row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" style="' + style + '">';
                        row += '<div class="cal-date">' + d.getDate() + '</div>';
                        if (hol) row += '<div class="cal-holiday-label">' + holidayMap[ds] + '</div>';

                        beats.forEach(function (b) {
                                var chipCls = 'cal-chip';
                                if (b.status === 'running') chipCls += ' running';
                                if (b.status === 'completed') chipCls += ' completed';
                                row += '<span class="' + chipCls + '" style="background:' + b.color + '">' + b.name + ' D' + b.day + (b.status === 'running' ? ' [live]' : '') + ' (' + b.visits + ')</span>';
                        });

                        if (isPicked) {
                                var idx = state.calPickedDates.indexOf(ds) + 1;
                                row += '<span class="cal-chip" style="background:#3b82f6;border:1px dashed #93c5fd;">Day ' + idx + '</span>';
                        }

                        row += '</td>';
                        if (inMonth) hasDay = true;
                        d.setDate(d.getDate() + 1);
                }
                row += '</tr>';
                if (hasDay) html += row;
        }
        html += '</table>';
        $('#cal-grid-content').html(html);
}

function renderBeatCards(beats) {
        var html = '';
        if (!beats || beats.length === 0) {
                html = '<p style="color:#64748b;font-size:12px;padding:20px;">No beats scheduled. Create a beat first.</p>';
        }
        (beats || []).forEach(function (b) {
                var badge = b.status === 'running' ? '<span class="status-badge status-running">Live</span>' :
                        b.status === 'completed' ? '<span class="status-badge status-completed">Done</span>' :
                                b.status === 'unscheduled' ? '<span class="status-badge" style="background:#f59e0b;color:#0f172a;">Unscheduled</span>' :
                                        '<span class="status-badge status-scheduled">Scheduled</span>';

                var totalV = 0, totalKm = 0;
                b.days.forEach(function (d) {
                        totalV += d.visitCount || 0;
                        totalKm += d.totalKm || 0;
                });

                html += '<div class="beat-card" data-plangroup="' + b.planGroupId + '">';
                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>';
                html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
                if (b.status !== 'running') {
                        var isScheduling = state.calSelectedBeat === b.planGroupId && state.calPickedDates.length > 0;
                        html += '<div class="beat-actions">';
                        if (isScheduling) {
                                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>';
                                html += '<button class="btn-sm btn-cancel-schedule" style="background:#475569;color:#e2e8f0;">Cancel</button>';
                        } else {
                                html += '<button class="btn-sm btn-schedule" data-pg="' + b.planGroupId + '" data-days="' + b.days.length + '">Schedule</button>';
                                html += '<button class="btn-sm btn-repeat" data-pg="' + b.planGroupId + '" data-days="' + b.days.length + '">Repeat</button>';
                        }
                        html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" data-pg="' + b.planGroupId + '" onclick="deleteBeat(\'' + b.planGroupId + '\')">Delete</button>';
                        html += '</div>';
                        if (isScheduling) {
                                html += '<div style="font-size:10px;color:#60a5fa;margin-top:4px;">Dates: ' + state.calPickedDates.join(', ') + '</div>';
                                html += '<div style="font-size:10px;color:#94a3b8;">Click calendar cells to adjust</div>';
                        }
                }
                html += '</div>';
        });
        $('#cal-beat-cards').html(html);
}

// Schedule button — suggest dates, show Confirm button in beat card
$(document).on('click', '.btn-schedule', function (e) {
        e.stopPropagation();
        var pg = $(this).data('pg');
        var days = parseInt($(this).data('days'));
        state.calSelectedBeat = pg;
        state.calPickedDates = [];

        $.ajax({
                url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
                data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
                success: function (r) {
                        var data = r.response || r;
                        state.calPickedDates = data.suggestedDates || [];
                        renderCalGrid(state.calData);
                        renderBeatCards(state.calData.scheduledBeats);
                        if (state.calPickedDates.length < days) {
                                alert('Only ' + state.calPickedDates.length + ' slots available this month. Need ' + days + '.');
                        }
                }
        });
});

// Click calendar cell to toggle picked date
$(document).on('click', '#cal-grid-content td:not(.blocked)', function () {
        if (!state.calSelectedBeat) return;
        var ds = $(this).data('date');
        var idx = state.calPickedDates.indexOf(ds);
        if (idx > -1) state.calPickedDates.splice(idx, 1);
        else state.calPickedDates.push(ds);
        state.calPickedDates.sort();
        renderCalGrid(state.calData);
        renderBeatCards(state.calData.scheduledBeats);
});

// Cancel scheduling mode
$(document).on('click', '.btn-cancel-schedule', function (e) {
        e.stopPropagation();
        state.calSelectedBeat = null;
        state.calPickedDates = [];
        renderCalGrid(state.calData);
        renderBeatCards(state.calData.scheduledBeats);
});

// Confirm schedule — save to server
$(document).on('click', '.btn-confirm-schedule', function (e) {
        e.stopPropagation();
        var pg = $(this).data('pg');
        var daysNeeded = parseInt($(this).data('days'));

        if (state.calPickedDates.length < daysNeeded) {
                alert('Need ' + daysNeeded + ' dates but only ' + state.calPickedDates.length + ' selected.');
                return;
        }

        var beat = (state.calData.scheduledBeats || []).find(function (b) {
                return b.planGroupId === pg;
        });
        if (!beat) return;

        $.ajax({
                url: context + "/beatPlan/scheduleOnCalendar", type: "POST",
                data: {
                        planGroupId: pg,
                        dates: JSON.stringify(state.calPickedDates.slice(0, daysNeeded)),
                        beatName: beat.beatName,
                        beatColor: beat.beatColor
                },
                success: function () {
                        state.calSelectedBeat = null;
                        state.calPickedDates = [];
                        loadCalendarData();
                },
                error: function (xhr) {
                        alert('Error: ' + (xhr.responseText || xhr.statusText));
                }
        });
});

// Repeat
$(document).on('click', '.btn-repeat', function (e) {
        e.stopPropagation();
        var pg = $(this).data('pg');
        var days = parseInt($(this).data('days'));

        $.ajax({
                url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
                data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
                success: function (r) {
                        var data = r.response || r;
                        if ((data.suggestedDates || []).length < days) {
                                alert('Not enough slots this month');
                                return;
                        }
                        if (!confirm('Repeat beat on: ' + data.suggestedDates.join(', ') + '?')) return;

                        $.ajax({
                                url: context + "/beatPlan/repeatBeat", type: "POST",
                                data: {sourcePlanGroupId: pg, authUserId: state.authUserId, dates: JSON.stringify(data.suggestedDates)},
                                success: function () {
                                        loadCalendarData();
                                        alert('Beat repeated!');
                                }
                        });
                }
        });
});

// ============ DELETE BEAT ============
function deleteBeat(planGroupId) {
        if (!confirm('Delete this beat? This cannot be undone.')) return;
        $.ajax({
                url: context + "/beatPlan/delete", type: "POST",
                data: {planGroupId: planGroupId},
                success: function () {
                        loadCalendarData();
                },
                error: function (xhr) {
                        alert('Error: ' + (xhr.responseText || xhr.statusText));
                }
        });
}

// ============ HELPERS ============
function curDay() {
        return state.days[state.currentDay - 1];
}

function haversine(lat1, lng1, lat2, lng2) {
        var R = 6371, dLat = (lat2 - lat1) * Math.PI / 180, dLng = (lng2 - lng1) * Math.PI / 180;
        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);
        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

function fmtMins(m) {
        return Math.floor(m / 60) + 'h ' + Math.round(m % 60) + 'm';
}

function fmtDate(d) {
        return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
}

function fitBounds() {
        if (Object.keys(markers).length > 0) {
                var b = new google.maps.LatLngBounds();
                if (homeMarker) b.extend(homeMarker.getPosition());
                for (var k in markers) b.extend(markers[k].getPosition());
                map.fitBounds(b);
        }
}