Subversion Repositories SmartDukaan

Rev

Rev 37060 | 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: [],
        // L4+ operators may schedule a beat on today's date (set from the page).
        canScheduleToday: (typeof canScheduleToday !== 'undefined' && !!canScheduleToday)
};

var VISIT_MINS = 30;
var AVG_SPEED = 30;
var DAY_LIMIT = 540;
var ROAD_FACTOR = 1.3;
var isSubmittingBeat = false; // guard against double-submit
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>';
                        // Show "Name (#authUserId)" so the head can quickly match identities
                        // (useful when grabbing an id for the Visit-Request Reassign prompt etc).
                        data.forEach(function (u) {
                                html += '<option value="' + u.id + '">' + u.name + ' (#' + u.id + ')</option>';
                        });
                        $('#bp-auth-user').html(html);
                }
        });
});

$('#bp-load').on('click', function () {
        var uid = $('#bp-auth-user').val();
        if (!uid) {
                alert('Select a user');
                return;
        }

        // Hard reset of any previous user's cached state so calendar chips,
        // route lines, partner markers, and the route-detail view don't leak.
        state.authUserId = parseInt(uid);
        state.categoryId = parseInt($('#bp-category').val());
        state.mode = 'list';
        state.days = [];
        state.partners = [];
        state.currentDay = 1;
        state.calMonth = null;
        state.calData = null;
        state.calSelectedBeat = null;
        state.calPickedDates = [];
        state.homeLat = null;
        state.homeLng = null;
        state.homeName = '';
        state.activeBaseLocationId = null;
        state.baseLocations = [];
        state.viewDate = null;
        state.originalDayCount = 0;
        state.visitRequests = [];
        state.assignVisitRequest = null;
        state.assignedLeads = [];
        state.assignLead = null;
        state.deferredItems = [];
        $('#visit-request-panel').hide();
        $('#assigned-leads-panel').hide();
        // A fresh load starts both collapsibles expanded.
        $('#vr-body, #al-body').show();
        $('#vr-caret, #al-caret').html('&#9662;');
        // #deferred-items-panel now sits inside its own tab pane (#panel-deferred)
        // which controls visibility — no need to hide the inner panel here.
        $('#di-list').empty();
        $('#di-count').text('');
        $('#di-tab-count').text('');
        // Clear the rendered calendar grid + route map / panel so nothing from
        // the previous user is briefly visible while the new fetch is in flight.
        $('#cal-grid-content').empty();
        $('#route-list').empty();
        if (typeof clearRoute === 'function') {
                try {
                        clearRoute();
                } catch (e) {
                }
        }
        if (typeof markers === 'object' && markers) {
                for (var k in markers) {
                        try {
                                markers[k].setMap(null);
                        } catch (e) {
                        }
                }
                markers = {};
        }
        if (typeof homeMarker !== 'undefined' && homeMarker) {
                try {
                        homeMarker.setMap(null);
                } catch (e) {
                }
                homeMarker = null;
        }

        $('#bp-user-label').text($('#bp-auth-user option:selected').text());

        // Load all saved base locations — default goes in state.home*, the rest is
        // exposed via a picker in the route panel so the planner can pick a
        // different starting point for this one beat without changing the default.
        $.ajax({
                url: context + "/beatPlan/listBaseLocations", type: "GET", dataType: "json",
                data: {authUserId: uid},
                success: function (r) {
                        var data = r.response || r;
                        state.baseLocations = data.locations || [];
                        var def = state.baseLocations.find(function (l) {
                                return l.isDefault;
                        }) || state.baseLocations[0];
                        if (def) {
                                state.homeLat = parseFloat(def.latitude);
                                state.homeLng = parseFloat(def.longitude);
                                state.homeName = def.locationName;
                                state.activeBaseLocationId = def.id;
                        } else {
                                state.activeBaseLocationId = null;
            }
            showBeatList();
                        // If the user is currently on the Calendar tab when Load is clicked,
                        // re-fetch immediately. Otherwise the calendar grid stays blank
                        // until they click the tab (which would auto-fetch on its own).
                        if ($('.panel-tab[data-tab="calendar"]').hasClass('active')) {
                                var now = new Date();
                                state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
                                loadCalendarData();
                        }
                        // Pending visit requests for this user's hierarchy. Hidden when empty.
                        if (typeof loadVisitRequests === 'function') {
                                loadVisitRequests();
                        }
                        // This user's active assigned leads (routable onto a beat). Hidden when empty.
                        if (typeof loadAssignedLeads === 'function') {
                                loadAssignedLeads();
                        }
                        // Deferred items (partners + leads) for this user. Hidden when empty.
                        if (typeof loadDeferredItems === 'function') {
                                loadDeferredItems();
                        }
        }
    });
});

// Called from the picker → swap the start location for the beat being
// created/edited. Updates state.home* + day 1's start point, then re-draws.
// Inline typeahead inside the route sidebar — same data source as the
// Partners tab (state.partners, fed by /beatPlan/getPartners, which mirrors
// the /getPartnerReadonlyInfo pattern used by the Partner Access tab).
// User types 2+ chars → suggestions appear → click adds to the current day.
function renderPartnerQuickAdd() {
        if (!state.partners || state.partners.length === 0) return '';
        return ''
                + '<div style="position:relative; margin-bottom:10px;">'
                + '  <input type="text" id="bp-partner-quick" autocomplete="off"'
                + '         placeholder="Search & add a partner..."'
                + '         style="width:100%; padding:8px 10px; border:1px solid #475569; border-radius:6px;'
                + '                background:#0f172a; color:#e2e8f0; font-size:12px; font-family:inherit;">'
                + '  <div id="bp-partner-suggest"'
                + '       style="display:none; position:absolute; left:0; right:0; top:100%; z-index:50;'
                + '              background:#1e293b; border:1px solid #334155; border-radius:6px;'
                + '              margin-top:4px; max-height:260px; overflow-y:auto;'
                + '              box-shadow:0 6px 16px rgba(0,0,0,0.4);"></div>'
                + '</div>';
}

function refreshPartnerSuggestions() {
        var $input = $('#bp-partner-quick');
        if (!$input.length) return;
        var q = ($input.val() || '').trim().toLowerCase();
        var $sug = $('#bp-partner-suggest');
        if (q.length < 2) {
                $sug.hide().empty();
                return;
        }

        // Lookup of fofoId → day number for partners already on the route
        var inRoute = {};
        (state.days || []).forEach(function (d) {
                (d.visits || []).forEach(function (v) {
                        if (!v.isLead && v.type !== 'lead') inRoute[v.id] = d.dayNumber;
                });
        });

        var matches = (state.partners || []).filter(function (p) {
                var hay = ((p.code || '') + ' '
                        + (p.outletName || '') + ' '
                        + (p.businessName || '') + ' '
                        + (p.city || '')).toLowerCase();
                return hay.indexOf(q) !== -1;
        }).slice(0, 10);

        if (matches.length === 0) {
                $sug.html('<div style="padding:10px; color:#94a3b8; font-size:11px; text-align:center;">No partners found</div>').show();
                return;
        }

        var html = '';
        matches.forEach(function (p) {
                var day = inRoute[p.fofoId];
                var nameLine = (p.outletName || p.businessName || '');
                if (p.city) nameLine += (nameLine ? ' - ' : '') + p.city;
                var inactiveBadge = (p.active === false)
                        ? ' <span style="background:#7c2d12;color:#fed7aa;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;">Inactive</span>'
                        : '';
                if (day) {
                        html += '<div class="bp-partner-row in-route" style="padding:8px 10px; font-size:11px;'
                                + ' color:#94a3b8; border-bottom:1px solid #0f172a; cursor:not-allowed;">'
                                + '<div style="font-weight:600;">' + (p.code || '') + inactiveBadge + '</div>'
                                + '<div>' + nameLine + ' <span style="color:#22c55e;">(in D' + day + ')</span></div>'
                                + '</div>';
                } else {
                        html += '<div class="bp-partner-row" data-fofoid="' + p.fofoId + '"'
                                + ' style="padding:8px 10px; font-size:12px; color:#e2e8f0;'
                                + ' border-bottom:1px solid #0f172a; cursor:pointer;">'
                                + '<div style="font-weight:600;">' + (p.code || '') + inactiveBadge + '</div>'
                                + '<div style="font-size:11px; color:#94a3b8;">' + nameLine + '</div>'
                                + '</div>';
                }
        });
        $sug.html(html).show();
}

$(document).on('input', '#bp-partner-quick', refreshPartnerSuggestions);

$(document).on('focus', '#bp-partner-quick', function () {
        if (($(this).val() || '').trim().length >= 2) refreshPartnerSuggestions();
});

// Hide on outside click
$(document).on('click', function (e) {
        if (!$(e.target).closest('#bp-partner-quick, #bp-partner-suggest').length) {
                $('#bp-partner-suggest').hide();
        }
});

$(document).on('mousedown', '#bp-partner-suggest .bp-partner-row', function (e) {
        if ($(this).hasClass('in-route')) return;
        e.preventDefault(); // keep input from losing focus before we read fofoid
        var fofoId = parseInt($(this).data('fofoid'));
        if (!fofoId) return;
        if (typeof addVisit === 'function') addVisit(fofoId, 'next');
        $('#bp-partner-quick').val('').focus();
        $('#bp-partner-suggest').hide().empty();
});

function renderBaseLocationPicker() {
        var locs = state.baseLocations || [];
        if (locs.length === 0) return '';
        var activeId = state.activeBaseLocationId;
        if (locs.length === 1) {
                var only = locs[0];
                return '<div style="font-size:11px;color:#94a3b8;margin-bottom:10px;">'
                        + 'Starting from: <strong style="color:#e2e8f0;">' + (only.locationName || 'Home') + '</strong>'
                        + (only.isDefault ? ' <span style="color:#22c55e;">(default)</span>' : '')
                        + '</div>';
        }
        var html = '<div style="margin-bottom:10px;">'
                + '<label style="font-size:11px;color:#94a3b8;display:block;margin-bottom:4px;">Start this beat from:</label>'
                + '<select id="bp-base-picker" style="width:100%;padding:6px 8px;border:1px solid #475569;border-radius:4px;background:#0f172a;color:#e2e8f0;font-size:12px;font-family:inherit;">';
        locs.forEach(function (l) {
                var sel = (l.id === activeId) ? ' selected' : '';
                var label = (l.locationName || 'Location ' + l.id) + (l.isDefault ? ' (default)' : '');
                html += '<option value="' + l.id + '"' + sel + '>' + label + '</option>';
        });
        html += '</select></div>';
        return html;
}

$(document).on('change', '#bp-base-picker', function () {
        var id = parseInt($(this).val());
        if (!id) return;
        applyBaseLocation(id);
});

function applyBaseLocation(id) {
        var loc = (state.baseLocations || []).find(function (l) {
                return l.id === id;
        });
        if (!loc) return;
        state.homeLat = parseFloat(loc.latitude);
        state.homeLng = parseFloat(loc.longitude);
        state.homeName = loc.locationName;
        state.activeBaseLocationId = id;

        // Only day 1 starts from the chosen base; later days start where the
        // previous day ended (existing behavior unchanged).
        if (state.days && state.days.length > 0) {
                state.days[0].startLat = state.homeLat;
                state.days[0].startLng = state.homeLng;
                state.days[0].startName = state.homeName;
                if (typeof recalcDay === 'function') {
                        var saved = state.currentDay;
                        state.currentDay = 1;
                        recalcDay();
                        state.currentDay = saved;
                }
        }

        // Move the home marker on the map + recenter so the user actually sees it.
        if (typeof google !== 'undefined' && google.maps && map) {
                var pos = new google.maps.LatLng(state.homeLat, state.homeLng);
                if (homeMarker) {
                        homeMarker.setPosition(pos);
                        homeMarker.setTitle('Home: ' + state.homeName);
                } else {
                        homeMarker = new google.maps.Marker({
                                map: map, position: pos, title: 'Home: ' + state.homeName,
                                icon: {
                                        url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
                                        scaledSize: new google.maps.Size(32, 32)
                                },
                                zIndex: 100
                        });
                }
                map.panTo(pos);
        }

        if (typeof drawRoute === 'function') drawRoute();
        if (typeof renderRoutePanel === 'function') renderRoutePanel();
        if (typeof updateBottomBar === 'function') updateBottomBar();
}

// ============ BEAT LIST VIEW ============
function showBeatList() {
    state.mode = 'list';
    $('#bottom-bar').hide();

    // Load all beats for this user
    var now = new Date();
    var month = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);

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

            var html = '<div style="margin-bottom:10px;">';
            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>';
            html += '</div>';

            if (beats.length === 0) {
                html += '<p style="color:#64748b;font-size:12px;text-align:center;padding:20px;">No beats created yet.</p>';
            }

            // Deduplicate by beatName
            var seen = {};
            beats.forEach(function (b) {
                var key = b.beatName || b.planGroupId;
                if (!seen[key]) {
                    seen[key] = {beat: b, count: 0};
                }
                seen[key].count++;
            });

            Object.keys(seen).forEach(function (key) {
                var item = seen[key];
                var b = item.beat;
                var totalV = 0, totalKm = 0;
                b.days.forEach(function (d) {
                    totalV += d.visitCount || 0;
                    totalKm += d.totalKm || 0;
                });

                var statusCls = b.status === 'running' ? 'status-running' : b.status === 'completed' ? 'status-completed' : 'status-scheduled';
                var statusLabel = b.status === 'running' ? 'Live' : b.status === 'completed' ? 'Done' : b.status === 'unscheduled' ? 'Unscheduled' : 'Scheduled';

                                html += '<div class="beat-card beat-list-item" data-plangroup="' + b.planGroupId + '" style="cursor:pointer; position:relative;">';
                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>';
                html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km';
                if (item.count > 1) html += ' | ' + item.count + 'x scheduled';
                html += '</div>';
                                html += '<button class="beat-edit-btn" data-plangroup="' + b.planGroupId + '" '
                                        + 'style="position:absolute;top:8px;right:8px;background:#475569;color:#e2e8f0;'
                                        + 'border:none;border-radius:4px;padding:3px 8px;font-size:10px;cursor:pointer;'
                                        + 'font-family:inherit;">Edit</button>';
                html += '</div>';
            });

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

// Click on existing beat → view its route on map
$(document).on('click', '.beat-list-item', function () {
    var pg = $(this).data('plangroup');
    viewBeat(pg);
});

// Edit button on a beat card → load in EDIT mode (modifiable)
$(document).on('click', '.beat-edit-btn', function (e) {
        e.stopPropagation(); // don't also trigger beat-list-item view
        var pg = $(this).data('plangroup');
        var date = $(this).data('date') || null;
        editBeat(String(pg), date ? String(date) : null);
});

// Loads a beat into edit mode — same UI as create but Finish becomes Save.
// When planDate is provided: leads scheduled on that date are loaded too and
// can be removed (template = partners stays the same across runs).
function editBeat(planGroupId, planDate) {
        state.mode = 'edit';
        state.editingBeatId = planGroupId;
        state.editingDate = planDate || null;
        state.viewDate = null;
        state.savedPlanGroupId = null;
        isSubmittingBeat = false;

        $.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 pData = r.response || r;
                        state.partners = pData.partners || [];
                        var partnerMap = {};
                        state.partners.forEach(function (p) {
                                partnerMap[p.fofoId] = p;
                        });

                        $.ajax({
                                url: context + '/beatPlan/calendar', type: 'GET', dataType: 'json',
                                data: {authUserId: state.authUserId, month: '2020-01'},
                                success: function (r2) {
                                        var calData = r2.response || r2;
                                        var beat = (calData.scheduledBeats || []).find(function (b) {
                                                return String(b.planGroupId) === String(planGroupId);
                                        });
                                        if (!beat) {
                                                alert('Beat not found');
                                                return;
                                        }
                                        state.beatTitle = beat.beatName || 'Beat';

                                        // Pass planDate to getBeatVisits so leads for that date are included
                                        var visitsData = planDate
                                                ? {planGroupId: planGroupId, planDate: planDate}
                                                : {planGroupId: planGroupId};

                                        $.ajax({
                                                url: context + '/beatPlan/getBeatVisits', type: 'GET', dataType: 'json',
                                                data: visitsData,
                                                success: function (r3) {
                                                        var visits = (r3.response || r3) || [];
                                                        var dayVisitsMap = {};
                                                        visits.forEach(function (v) {
                                                                if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
                                                                dayVisitsMap[v.dayNumber].push(v);
                                                        });

                                                        // Collect lead IDs so we can fetch their name/geo in parallel
                                                        var leadIdsToFetch = [];
                                                        visits.forEach(function (v) {
                                                                if (v.visitType === 'lead') leadIdsToFetch.push(v.fofoId);
                                                        });

                                                        state.days = [];
                                                        var seen = {};
                                                        beat.days.forEach(function (d) {
                                                                if (seen[d.dayNumber]) return;
                                                                seen[d.dayNumber] = true;
                                                                var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
                                                                        return a.sequenceOrder - b.sequenceOrder;
                                                                });
                                                                var builtVisits = [];
                                                                dayVisits.forEach(function (v) {
                                                                        if (v.visitType === 'lead') {
                                                                                builtVisits.push({
                                                                                        id: v.fofoId, type: 'lead',
                                                                                        name: 'LEAD #' + v.fofoId, code: 'LEAD',
                                                                                        lat: null, lng: null, isLead: true
                                                                                });
                                                                        } else if (v.visitType === 'office') {
                                                                                // Office stops are enriched server-side because the partner
                                                                                // map doesn't cover them. Lat/lng come from logistics.company_office.
                                                                                builtVisits.push({
                                                                                        id: v.fofoId, type: 'office',
                                                                                        name: (v.code ? v.code + ' - ' : '') + (v.name || 'Office #' + v.fofoId),
                                                                                        code: v.code || 'OFFICE',
                                                                                        lat: v.latitude ? parseFloat(v.latitude) : null,
                                                                                        lng: v.longitude ? parseFloat(v.longitude) : null,
                                                                                        isOffice: true
                                                                                });
                                                                        } else {
                                                                                var p = partnerMap[v.fofoId];
                                                                                if (p) builtVisits.push({
                                                                                        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 1 starts at the beat's own saved start location when set;
                                                                // otherwise fall back to the user's default base. Later days
                                                                // start where the prior day ended (handled by the Day Break flow).
                                                                var dayStartLat = state.homeLat, dayStartLng = state.homeLng, dayStartName = state.homeName;
                                                                if (d.dayNumber === 1) {
                                                                        var bLat = beat.startLatitude != null ? parseFloat(beat.startLatitude) : NaN;
                                                                        var bLng = beat.startLongitude != null ? parseFloat(beat.startLongitude) : NaN;
                                                                        if (!isNaN(bLat) && !isNaN(bLng)) {
                                                                                dayStartLat = bLat;
                                                                                dayStartLng = bLng;
                                                                                dayStartName = beat.startLocationName || state.homeName;
                                                                        }
                                                                }
                                                                state.days.push({
                                                                        dayNumber: d.dayNumber,
                                                                        startLat: dayStartLat, startLng: dayStartLng,
                                                                        startName: dayStartName,
                                                                        visits: builtVisits,
                                                                        endAction: d.endAction || (d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME'),
                                                                        totalKm: 0, totalMins: 0
                                                                });
                                                        });
                                                        state.currentDay = 1;

                                                        // Snapshot the originals — used during save to detect day-count
                                                        // increases (blocked) and lead removals (popup with cancel/reschedule).
                                                        state.originalDayCount = state.days.length;
                                                        state.originalLeads = [];
                                                        state.days.forEach(function (d) {
                                                                d.visits.forEach(function (v) {
                                                                        if (v.type === 'lead' || v.isLead) {
                                                                                state.originalLeads.push({
                                                                                        leadId: v.id,
                                                                                        dayNumber: d.dayNumber,
                                                                                        name: v.name || ('Lead #' + v.id)
                                                                                });
                                                                        }
                                                                });
                                                        });

                                                        // Async enrich each lead with name + geo
                                                        var leadPromises = [];
                                                        state.days.forEach(function (day) {
                                                                day.visits.forEach(function (v) {
                                                                        if (!v.isLead) return;
                                                                        leadPromises.push(
                                                                                $.get(context + '/lead-geo/check/' + v.id).then(function (rr) {
                                                                                        var g = rr.response || rr;
                                                                                        if (g && g.hasApprovedGeo) {
                                                                                                v.lat = g.latitude;
                                                                                                v.lng = g.longitude;
                                                                                        }
                                                                                }),
                                                                                $.get(context + '/getLead?leadId=' + v.id).then(function (rr) {
                                                                                        var l = rr.response || rr;
                                                                                        if (l && l.firstName) {
                                                                                                var nm = 'LEAD - ' + l.firstName + ' ' + (l.lastName || '');
                                                                                                v.name = nm;
                                                                                                // Keep the originalLeads snapshot label in sync so the
                                                                                                // removed-leads popup shows the real name, not 'LEAD #123'.
                                                                                                (state.originalLeads || []).forEach(function (ol) {
                                                                                                        if (ol.leadId === v.id) ol.name = nm;
                                                                                                });
                                                                                        }
                                                                                })
                                                                        );
                                                                });
                                                        });

                                                        $.when.apply($, leadPromises).always(function () {
                                                                // Pre-select whichever saved base location matches the beat's
                                                                // existing day-1 start (lat/lng), so the picker reflects current state.
                                                                var day1 = state.days[0];
                                                                if (day1 && state.baseLocations) {
                                                                        var match = state.baseLocations.find(function (l) {
                                                                                return Math.abs(parseFloat(l.latitude) - day1.startLat) < 1e-4
                                                                                        && Math.abs(parseFloat(l.longitude) - day1.startLng) < 1e-4;
                                                                        });
                                                                        if (match) state.activeBaseLocationId = match.id;
                                                                }

                                                                // renderRoutePanel now builds the edit header (back btn, EDITING
                                                                // title, date label, base picker, partner search) inline — so we
                                                                // just call it without the one-time prepend that used to live here.
                                                                renderRoutePanel();

                                                                initMap();
                                                                // Add lead markers (orange) on the map
                                                                state.days.forEach(function (day) {
                                                                        day.visits.forEach(function (v) {
                                                                                if (v.isLead && v.lat && v.lng) {
                                                                                        var m = new google.maps.Marker({
                                                                                                map: map,
                                                                                                position: {lat: v.lat, lng: v.lng},
                                                                                                title: v.name,
                                                                                                label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
                                                                                                icon: {
                                                                                                        path: google.maps.SymbolPath.CIRCLE, scale: 16,
                                                                                                        fillColor: '#e67e22', fillOpacity: 0.95,
                                                                                                        strokeColor: '#fff', strokeWeight: 2
                                                                                                }
                                                                                        });
                                                                                        markers[v.id] = m;
                                                                                }
                                                                        });
                                                                });
                                                                resetMarkerColors();
                                                                drawRoute();
                                                                updateBottomBar();
                                                                $('#bottom-bar').show();        // make Save bar visible
                                                                $('#btn-finish').text('Save Changes').prop('disabled', false);
                                                        });
                                                }
                                        });
                                }
                        });
                }
        });
}

function viewBeat(planGroupId, planDate) {
    state.mode = 'view';
        state.viewDate = planDate || null; // when set, show that run's leads
    $('#bottom-bar').hide();

    // Load partners first (for coordinates), then load beat visits
    $.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 pData = r.response || r;
            state.partners = pData.partners || [];

            // Build partner lookup by fofoId
            var partnerMap = {};
            state.partners.forEach(function (p) {
                partnerMap[p.fofoId] = p;
            });

            // Load beat visits
            $.ajax({
                url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
                data: {authUserId: state.authUserId, month: '2020-01'},
                success: function (r2) {
                    var calData = r2.response || r2;
                    var beat = (calData.scheduledBeats || []).find(function (b) {
                                                return String(b.planGroupId) === String(planGroupId);
                    });
                    if (!beat) {
                        alert('Beat not found');
                        return;
                    }

                    // Reconstruct state.days from beat data for map drawing
                    // We need actual visit fofoIds — fetch from beat_plan records
                    $.ajax({
                        url: context + "/beatPlan/getBeatVisits", type: "GET", dataType: "json",
                                                data: planDate
                                                        ? {planGroupId: planGroupId, planDate: planDate}
                                                        : {planGroupId: planGroupId},
                        success: function (r3) {
                            var visits = (r3.response || r3) || [];

                            // Group visits by dayNumber
                            var dayVisitsMap = {};
                            visits.forEach(function (v) {
                                if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
                                dayVisitsMap[v.dayNumber].push(v);
                            });

                                                        // Build days for route display.
                                                        // A beat scheduled on multiple dates has multiple schedule
                                                        // rows per day_number — dedupe so each day appears once.
                            state.days = [];
                                                        var seenDayNumbers = {};
                            beat.days.forEach(function (d) {
                                                                if (seenDayNumbers[d.dayNumber]) return; // skip duplicate day
                                                                seenDayNumbers[d.dayNumber] = true;

                                var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
                                    return a.sequenceOrder - b.sequenceOrder;
                                });
                                var startLat = state.homeLat, startLng = state.homeLng, startName = state.homeName;

                                // If not first day, start from previous day's last visit
                                if (state.days.length > 0) {
                                    var prevDay = state.days[state.days.length - 1];
                                    if (prevDay.visits.length > 0 && prevDay.endAction === 'DAYBREAK') {
                                        var lastV = prevDay.visits[prevDay.visits.length - 1];
                                        startLat = lastV.lat;
                                        startLng = lastV.lng;
                                        startName = lastV.code || lastV.name;
                                    }
                                }

                                var builtVisits = [];
                                dayVisits.forEach(function (v) {
                                    if (v.visitType === 'lead') {
                                                                                builtVisits.push({
                                            id: v.fofoId, type: 'lead',
                                            name: 'LEAD #' + v.fofoId,
                                            code: 'LEAD',
                                            lat: null, lng: null,
                                            isLead: true
                                        });
                                                                        } else if (v.visitType === 'office') {
                                                                                builtVisits.push({
                                                                                        id: v.fofoId, type: 'office',
                                                                                        name: (v.code ? v.code + ' - ' : '') + (v.name || 'Office #' + v.fofoId),
                                                                                        code: v.code || 'OFFICE',
                                                                                        lat: v.latitude ? parseFloat(v.latitude) : null,
                                                                                        lng: v.longitude ? parseFloat(v.longitude) : null,
                                                                                        isOffice: true
                                                                                });
                                    } else {
                                        var p = partnerMap[v.fofoId];
                                        if (p) {
                                            builtVisits.push({
                                                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
                                            });
                                        }
                                    }
                                });

                                state.days.push({
                                    dayNumber: d.dayNumber,
                                    startLat: startLat,
                                    startLng: startLng,
                                    startName: startName,
                                    visits: builtVisits,
                                                                        // Use the actual end_action stored on this schedule row.
                                                                        // The dayNumber-based inference is wrong for multi-instance
                                                                        // beats (same beat scheduled on several dates) — every
                                                                        // instance would have day N but only one was treated HOME.
                                                                        endAction: d.endAction || (d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME'),
                                    totalKm: d.totalKm || 0,
                                    totalMins: d.totalMins || 0
                                });
                            });

                            state.currentDay = 1;

                                                        // Collect all lead data promises across all days
                                                        var allLeadPromises = [];
                                                        state.days.forEach(function (d) {
                                                                d.visits.forEach(function (v) {
                                                                        if (v.isLead) {
                                                                                allLeadPromises.push(
                                                                                        $.get(context + '/lead-geo/check/' + v.id).then(function (resp) {
                                                                                                var geoData = resp.response || resp;
                                                                                                if (geoData && geoData.hasApprovedGeo) {
                                                                                                        v.lat = geoData.latitude;
                                                                                                        v.lng = geoData.longitude;
                                                                                                }
                                                                                        })
                                                                                );
                                                                                allLeadPromises.push(
                                                                                        $.get(context + '/getLead?leadId=' + v.id).then(function (resp) {
                                                                                                var lead = resp.response || resp;
                                                                                                if (lead && lead.firstName) {
                                                                                                        v.name = 'LEAD - ' + lead.firstName + ' ' + (lead.lastName || '');
                                                                                                }
                                                                                        })
                                                                                );
                                                                        }
                                                                });
                                                        });

                                                        // Wait for all lead data, then render
                                                        $.when.apply($, allLeadPromises).always(function () {
                                                                // Render left panel
                                                                state.mode = 'view';
                                                                renderRoutePanel();
                                                                var dateLabel = state.viewDate
                                                                        ? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Run date: ' + state.viewDate + ' (showing this day\'s leads)</div>'
                                                                        : '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Beat template (partners only)</div>';
                                                                $('#route-list').prepend('<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back to list</button></div><div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:4px;">' + beat.beatName + '</div>' + dateLabel);

                                                                // Init map with all partners + lead markers
                                                                initMap();
                                                                state.days.forEach(function (d) {
                                                                        d.visits.forEach(function (v) {
                                                                                if (v.isLead && v.lat && v.lng) {
                                                                                        var m = new google.maps.Marker({
                                                                                                map: map,
                                                                                                position: {lat: v.lat, lng: v.lng},
                                                                                                title: v.name,
                                                                                                label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
                                                                                                icon: {
                                                                                                        path: google.maps.SymbolPath.CIRCLE,
                                                                                                        scale: 16,
                                                                                                        fillColor: '#e67e22',
                                                                                                        fillOpacity: 0.95,
                                                                                                        strokeColor: '#fff',
                                                                                                        strokeWeight: 2
                                                                                                }
                                                                                        });
                                                                                        markers[v.id] = m;
                                                                                }
                                                                        });
                                                                });
                                                                resetMarkerColors();
                                                                drawRoute();
                                                        });
                        }
                    });
                }
            });
        }
    });
}

// Back to list
$(document).on('click', '.btn-back-to-list', function () {
    showBeatList();
    // Clear map
    if (map) {
        for (var key in markers) {
            markers[key].setMap(null);
        }
        markers = {};
        clearRoute();
    }
});

// New Beat button
// Start the full map-first new-beat planner (shared by the big "+ New Beat"
// button and the "+ New beat" buttons on the visit-request / assigned-lead cards).
function startNewBeat() {
    var beatTitle = prompt('Enter beat name:', '');
    if (!beatTitle || !beatTitle.trim()) return;

    state.beatTitle = beatTitle.trim();
    state.mode = 'create';
        state.savedPlanGroupId = null; // reset — this is a fresh beat
        state.days = [];               // clear any leftover route data
        isSubmittingBeat = false;

    // Cards live on the Route tab already, but ensure we're there when invoked.
    $('.panel-tab[data-tab="route"]').click();

    if (!state.homeLat) {
        showHomeModal();
    } else {
        loadPartners();
    }
}

$(document).on('click', '#btn-new-beat', startNewBeat);

// ============ 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;
        if (state.mode === 'create' || state.mode === 'edit') $('#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 + ' Minutes Discussion Time</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 += '</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);

        // Auto-order the day into a nearest-neighbor route. The just-added stop may
        // no longer be last, so recolour + renumber every current-day marker by its
        // new index (resetMarkerColors handles both) rather than labelling this one.
        sortDayByNearestNeighbor(day);
        resetMarkerColors();

        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 = [];
}

var DAY_ROUTE_COLORS = ['#3b82f6', '#a855f7', '#06b6d4', '#ec4899', '#f59e0b', '#14b8a6'];

function drawRoute() {
        clearRoute();

    // Draw routes for ALL days, not just current
    state.days.forEach(function (day, dayIdx) {
        var isCurrent = day.dayNumber === state.currentDay;
        var routeColor = DAY_ROUTE_COLORS[dayIdx % DAY_ROUTE_COLORS.length];
        var opacity = isCurrent ? 0.9 : 0.5;
        var weight = isCurrent ? 3 : 2;

        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});
        });

        // Draw visit route lines for this day
        for (var i = 0; i < pts.length - 1; i++) {
            var line = new google.maps.Polyline({
                path: [pts[i], pts[i + 1]], geodesic: true,
                strokeColor: routeColor, strokeOpacity: opacity, strokeWeight: weight, map: map
            });
            routeLines.push(line);

            // Only show distance labels on current day
            if (isCurrent) {
                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);
            }
        }

        // Draw return-to-home line (not on DAYBREAK days)
        if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
            var lastV = day.visits[day.visits.length - 1];
            if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
                var retLine = new google.maps.Polyline({
                    path: [{lat: lastV.lat, lng: lastV.lng}, {lat: state.homeLat, lng: state.homeLng}],
                    geodesic: true, strokeColor: '#22c55e', strokeOpacity: isCurrent ? 0.4 : 0.2, strokeWeight: 2,
                    icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
                    map: map
                });
                routeLines.push(retLine);
            }
        }
    });
}

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();
}

// Auto-order a day's stops into a nearest-neighbor route from the day's start
// point (beat start / base for day 1, prior day's end for later days). Reorders
// day.visits in place; sequence = array index downstream. Stops without valid
// coords (e.g. leads with no captured geo) are parked at the end in their
// existing relative order rather than dropped.
function sortDayByNearestNeighbor(day) {
        if (!day || !day.visits || day.visits.length < 2) return;

        var routable = [], parked = [];
        day.visits.forEach(function (v) {
                if (typeof v.lat === 'number' && typeof v.lng === 'number' && !isNaN(v.lat) && !isNaN(v.lng)) routable.push(v);
                else parked.push(v);
        });

        var haveStart = (typeof day.startLat === 'number' && typeof day.startLng === 'number' && !isNaN(day.startLat) && !isNaN(day.startLng));
        var ordered = [], curLat, curLng;
        if (haveStart) {
                curLat = day.startLat;
                curLng = day.startLng;
        } else if (routable.length > 0) {
                var first = routable.shift();
                ordered.push(first);
                curLat = first.lat;
                curLng = first.lng;
        }

        while (routable.length > 0) {
                var bestIdx = 0, bestD = Infinity;
                for (var i = 0; i < routable.length; i++) {
                        var d = haversine(curLat, curLng, routable[i].lat, routable[i].lng);
                        if (d < bestD) { bestD = d; bestIdx = i; }
                }
                var next = routable.splice(bestIdx, 1)[0];
                ordered.push(next);
                curLat = next.lat;
                curLng = next.lng;
        }

        day.visits.length = 0;
        ordered.forEach(function (v) { day.visits.push(v); });
        parked.forEach(function (v) { day.visits.push(v); });
}

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 = '';
    if (state.mode === 'create' && state.beatTitle) {
        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>';
        html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + state.beatTitle + '</div>';
                html += renderBaseLocationPicker();
                html += renderPartnerQuickAdd();
        } else if (state.mode === 'edit' && state.beatTitle) {
                // Edit header used to be injected via a one-time prepend() in editBeat's
                // success callback — that meant any addVisit-triggered re-render wiped the
                // back button / title / base picker / search box. Building it here keeps
                // them present across every re-render so users can keep adding partners.
                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>';
                html += '<div style="font-size:14px;font-weight:600;color:#f59e0b;margin-bottom:4px;">EDITING: ' + state.beatTitle + '</div>';
                html += state.editingDate
                        ? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Editing run on: ' + state.editingDate + ' (leads on this date can be removed)</div>'
                        : '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Editing beat template (partners only)</div>';
                html += renderBaseLocationPicker();
                html += renderPartnerQuickAdd();
    }
        state.days.forEach(function (day) {
                var isCurrent = day.dayNumber === state.currentDay;
        var isLastDay = day.dayNumber === state.days.length;
                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 || 'Home') + '</div></div></div>';

                // Visits with distance between each
                var prevLat = day.startLat, prevLng = day.startLng;
        var isLastStop;
                day.visits.forEach(function (v, i) {
            isLastStop = (i === day.visits.length - 1);

                        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) {
                                // Show drive ETA and discussion-time dwell separately so the
                                // planner can see where the time goes.
                                html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">'
                                        + segDist.toFixed(1) + ' km | '
                                        + fmtMins(segTime) + ' drive '
                                        + '<span style="color:#94a3b8;">+ ' + VISIT_MINS + ' Minutes Discussion Time</span>'
                                        + '</span></div>';
                        }

            var isLeadVisit = v.isLead || v.type === 'lead';
                        // Outlet/business name is the primary label; code is the muted subtitle.
                        // v.name is stored as "CODE - Outlet Name" — strip the code prefix.
                        var outletLabel = v.name || '';
                        if (v.code && outletLabel.indexOf(v.code + ' - ') === 0) {
                                outletLabel = outletLabel.substring(v.code.length + 3);
                        }
                        if (!outletLabel) outletLabel = v.code || '';
            html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;' + (isLeadVisit ? 'background:#fff3cd;border-left:3px solid #e67e22;' : '') + '">';
            html += '<span class="stop-num" style="' + (isLeadVisit ? 'background:#e67e22;' : '') + '">' + (i + 1) + '</span>';
                        html += '<div class="stop-info"><div class="stop-name">' + outletLabel + (isLeadVisit ? ' <span style="background:#e67e22;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;margin-left:4px;letter-spacing:0.3px;">LEAD VISIT</span>' : '') + '</div>';
                        html += '<div class="stop-meta">' + (v.code || '') + '</div>';

            // Show "Last Visit" + "Day Break" buttons on last stop of current day
            if (isLastStop && isCurrent && !day.endAction) {
                html += '<div style="margin-top:4px;display:flex;gap:4px;">';
                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>';
                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>';
                html += '</div>';
            }

            html += '</div>';
                        if (state.mode === 'create' || state.mode === 'edit') {
                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;
                });

        // Show return-to-home only when day is complete (Last Visit) or still in progress
        if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
                        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) {
                                // Return-home leg = travel time only (no discussion at home).
                                html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">'
                                        + retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + ' drive (return)'
                                        + '</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>';
                }

        // Day break indicator
        if (day.endAction === 'DAYBREAK' && day.visits.length > 0) {
            html += '<div style="margin-left:12px;padding:4px 8px;font-size:11px;color:#f59e0b;font-weight:600;">-- Day Break (stays near last visit) --</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;
        });

        // Re-route the remaining stops (nearest-neighbor) before re-numbering below.
        sortDayByNearestNeighbor(day);

        // 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();
});

// ============ LAST VISIT (from sidebar) — marks beat as complete, return home ============
$(document).on('click', '.btn-last-visit', function (e) {
    e.stopPropagation();
    var dayNum = parseInt($(this).data('daynum'));
    var day = state.days[dayNum - 1];
    if (!day || day.visits.length === 0) return;

    var lastVisit = day.visits[day.visits.length - 1];
    if (!confirm('Mark ' + (lastVisit.code || lastVisit.name) + ' as last visit? Beat will end here and return home.')) return;

    day.endAction = 'HOME';
    drawRoute();
    renderRoutePanel();
    updateBottomBar();
});

// ============ DAY BREAK (from sidebar) — end day, next day starts from last visit ============
$(document).on('click', '.btn-day-break', function (e) {
    e.stopPropagation();
    var dayNum = parseInt($(this).data('daynum'));
    var day = state.days[dayNum - 1];
    if (!day || day.visits.length === 0) return;

    var lastVisit = day.visits[day.visits.length - 1];
    if (!confirm('Day Break at ' + (lastVisit.code || lastVisit.name) + '? Next day will start from this location.')) return;

    day.endAction = 'DAYBREAK';

    // Next day starts from LAST VISIT location (not home)
    var nextNum = state.days.length + 1;
    state.days.push({
        dayNumber: nextNum,
        startLat: lastVisit.lat, startLng: lastVisit.lng, startName: lastVisit.code || lastVisit.name,
        visits: [], endAction: null,
        stayName: null, stayLat: null, stayLng: null,
        totalKm: 0, totalMins: 0
    });
    state.currentDay = nextNum;

    resetMarkerColors();
    drawRoute();
        renderRoutePanel();
    updateBottomBar();
});

$('#btn-finish').on('click', function () {
        // Guard 1: already submitting — ignore duplicate clicks
        if (isSubmittingBeat) {
                return;
        }
        // Guard 2: this beat was already saved — don't re-submit
        if (state.savedPlanGroupId) {
                alert('This beat is already saved.');
                showBeatList();
                return;
        }
        if (state.days.every(function (d) {
                return d.visits.length === 0;
        })) {
                alert('No visits added');
                return;
        }

        var beatTitle = state.beatTitle || 'Beat';
        var isEdit = state.mode === 'edit' && state.editingBeatId;

        // EDIT RULE: cannot grow day count. Caught client-side so the modal
        // doesn't even open; server enforces the same rule.
        if (isEdit && state.originalDayCount && state.days.length > state.originalDayCount) {
                alert('Cannot increase the number of days while editing.\n\n'
                        + 'Original: ' + state.originalDayCount + ' day(s)\n'
                        + 'Current:  ' + state.days.length + ' day(s)\n\n'
                        + 'Please create a new beat for additional days.');
                return;
        }

        // Build plan data — recompute every day's totals + per-leg distance/time
        // fresh from the current state. recalcDay() only refreshes the CURRENT day,
        // so older days could carry stale values; doing it here guarantees DB rows
        // match what the user sees in the panel.
        var daysData = [];
        state.days.forEach(function (day) {
                if (day.visits.length === 0) return;

                // Walk the visits in order, computing each leg from the previous point.
                // Day always starts at (startLat, startLng); for the LAST stop, if
                // endAction === 'HOME' we ALSO add the return-to-home leg into the day total
                // (but not stored per-visit, just rolled into totalDistanceKm/totalTimeMins).
                var prevLat = day.startLat, prevLng = day.startLng;
                var dayKm = 0, dayMins = 0;
                var visitsOut = [];
                day.visits.forEach(function (v) {
                        var legKm = null, legMins = null;
                        if (prevLat != null && prevLng != null && v.lat != null && v.lng != null) {
                                legKm = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
                                legMins = Math.round((legKm / AVG_SPEED) * 60);
                                dayKm += legKm;
                                dayMins += legMins;
                        }
                        visitsOut.push({
                                id: v.id,
                                type: v.type || 'partner',
                                distanceFromPrevKm: legKm != null ? Number(legKm.toFixed(3)) : null,
                                timeFromPrevMins: legMins
                        });
                        if (v.lat != null && v.lng != null) {
                                prevLat = v.lat;
                                prevLng = v.lng;
                        }
                });
                // Return-home leg if applicable
                if (day.endAction === 'HOME' && day.visits.length > 0
                        && state.homeLat != null && state.homeLng != null) {
                        var last = day.visits[day.visits.length - 1];
                        if (last.lat != null && last.lng != null) {
                                var retKm = haversine(last.lat, last.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
                                dayKm += retKm;
                                dayMins += (retKm / AVG_SPEED) * 60;
                        }
                }
                // Add per-stop dwell time
                dayMins += day.visits.length * VISIT_MINS;

                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: Number(dayKm.toFixed(3)),
                        totalTimeMins: Math.round(dayMins),
                        visits: visitsOut
                });
        });

        // EDIT RULE: detect removed leads → show popup with cancel/reschedule per lead
        var removedLeads = [];
        if (isEdit && state.originalLeads && state.originalLeads.length) {
                var currentLeadIds = {};
                state.days.forEach(function (d) {
                        d.visits.forEach(function (v) {
                                if (v.type === 'lead' || v.isLead) currentLeadIds[v.id] = true;
                        });
                });
                removedLeads = state.originalLeads.filter(function (l) {
                        return !currentLeadIds[l.leadId];
                });
        }

        if (removedLeads.length > 0) {
                openRemovedLeadsModal(removedLeads, function (actions) {
                        doFinishSubmit(daysData, beatTitle, isEdit, actions);
                });
                return;
        }

        doFinishSubmit(daysData, beatTitle, isEdit, null);
});

function doFinishSubmit(daysData, beatTitle, isEdit, removedLeadActions) {
        var planPayload = {
                days: daysData,
                dates: daysData.map(function () {
                        return null;
                }),
                beatName: beatTitle.trim()
        };
        if (state.mode === 'edit' && state.editingDate) planPayload.planDate = state.editingDate;
        if (removedLeadActions && removedLeadActions.length) {
                planPayload.removedLeadActions = JSON.stringify(removedLeadActions);
        }
        var planData = JSON.stringify(planPayload);

        isSubmittingBeat = true;
        $('#btn-finish').prop('disabled', true).text(isEdit ? 'Updating...' : 'Saving...');

        var url = isEdit ? '/beatPlan/updateBeat' : '/beatPlan/submitPlan';
        var ajaxData = isEdit
                ? {beatId: state.editingBeatId, planData: planData}
                : {authUserId: state.authUserId, planData: planData};

        $.ajax({
                url: context + url,
                type: "POST",
                data: ajaxData,
                success: function (response) {
                        var data = (typeof response === 'string' ? JSON.parse(response) : response);
                        var result = data.response || data;

                        if (result.status) {
                                state.savedPlanGroupId = result.planGroupId;
                                $('#bottom-bar').hide();

                                if (result.duplicate) {
                                        alert('This beat already exists.');
                                } else if (isEdit) {
                                        var extras = [];
                                        if (result.leadsCancelled) extras.push(result.leadsCancelled + ' lead(s) cancelled');
                                        if (result.leadsRescheduled) extras.push(result.leadsRescheduled + ' lead(s) rescheduled');
                                        alert('Beat "' + beatTitle + '" updated successfully'
                                                + (extras.length ? '\n\n' + extras.join('\n') : '') + '.');
                                } else {
                                        alert('Beat "' + beatTitle + '" saved successfully!');
                                }

                                // Wipe in-progress beat state for this user — the next "+ New Beat"
                                // must start from a clean slate (no leftover days/visits/title/leads).
                                state.editingBeatId = null;
                                state.editingDate = null;
                                state.beatTitle = null;
                                state.days = [];
                                state.currentDay = 1;
                                state.originalDayCount = null;
                                state.originalLeads = [];
                                state.savedPlanGroupId = null;

                                // Clear map artifacts from the just-saved route so the canvas
                                // doesn't show stale polylines/labels behind the beat list.
                                routeLines.forEach(function (l) {
                                        l.setMap(null);
                                });
                                routeLines = [];
                                if (previewLine) {
                                        previewLine.setMap(null);
                                        previewLine = null;
                                }
                                if (typeof resetMarkerColors === 'function') resetMarkerColors();

                                state.mode = 'list';
                                showBeatList();
                        } else {
                                $('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
                                alert('Error saving beat plan');
                        }
                        isSubmittingBeat = false;
                },
                error: function (xhr) {
                        isSubmittingBeat = false;
                        $('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
                        // Surface a clean server message if present (e.g., day-increase block, missing beat on reschedule date)
                        var msg = xhr.responseText || xhr.statusText;
                        try {
                                var err = JSON.parse(xhr.responseText);
                                if (err && err.response) {
                                        if (typeof err.response === 'string') msg = err.response;
                                        else if (err.response.message) msg = err.response.message;
                                }
                        } catch (_) {
                        }
                        alert(msg);
                }
        });
}

// ============ REMOVED LEADS POPUP ============
// Shown when an edit removes one or more leads. Per lead, the user picks:
//   - Cancel  → mark CANCELLED
//   - Reschedule + date → move to whichever beat the same user has on that date
// The date input is validated against /beatPlan/userBeatsOnDate so the user
// sees "No beat on this date — pick another" before submit.
function openRemovedLeadsModal(removedLeads, onConfirm) {
        $('#modal-removed-leads').remove(); // clean any prior instance

        var rowsHtml = removedLeads.map(function (l) {
                return ''
                        + '<div class="rl-row" data-leadid="' + l.leadId + '" style="border:1px solid #334155;border-radius:6px;padding:8px;margin-bottom:8px;">'
                        + '  <div style="font-weight:600;color:#f1f5f9;margin-bottom:6px;">'
                        + '    ' + (l.name || ('Lead #' + l.leadId)) + ' <span style="color:#64748b;font-weight:400;font-size:11px;">(was on day ' + l.dayNumber + ')</span>'
                        + '  </div>'
                        + '  <label style="display:inline-flex;align-items:center;gap:6px;margin-right:14px;cursor:pointer;font-size:12px;">'
                        + '    <input type="radio" name="rl-act-' + l.leadId + '" value="cancel" checked> Cancel this lead'
                        + '  </label>'
                        + '  <label style="display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;">'
                        + '    <input type="radio" name="rl-act-' + l.leadId + '" value="reschedule"> Reschedule to another date'
                        + '  </label>'
                        + '  <div class="rl-date-wrap" style="display:none;margin-top:6px;">'
                        + '    <input type="date" class="rl-date" min="' + fmtDate(new Date()) + '" style="width:auto;display:inline-block;margin-bottom:0;">'
                        + '    <span class="rl-date-status" style="font-size:11px;margin-left:8px;"></span>'
                        + '  </div>'
                        + '</div>';
        }).join('');

        var html = ''
                + '<div class="modal-overlay show" id="modal-removed-leads">'
                + '  <div class="modal-box" style="min-width:520px;max-width:680px;">'
                + '    <h3>You removed ' + removedLeads.length + ' lead(s) from this beat</h3>'
                + '    <p style="font-size:12px;color:#94a3b8;margin-bottom:12px;">'
                + '      Choose what to do with each lead before saving. Reschedule moves the lead to whichever beat this user has on the selected date.'
                + '    </p>'
                + '    <div id="rl-list" style="max-height:55vh;overflow-y:auto;">' + rowsHtml + '</div>'
                + '    <div class="modal-actions">'
                + '      <button class="modal-cancel" id="rl-cancel">Cancel (keep editing)</button>'
                + '      <button class="modal-confirm" id="rl-confirm">Save with these actions</button>'
                + '    </div>'
                + '  </div>'
                + '</div>';
        $('body').append(html);

        // Show/hide date picker per row
        $('#modal-removed-leads').on('change', 'input[type="radio"]', function () {
                var $row = $(this).closest('.rl-row');
                var isResched = $row.find('input[type="radio"]:checked').val() === 'reschedule';
                $row.find('.rl-date-wrap').toggle(isResched);
                $row.find('.rl-date-status').text('');
        });

        // Validate the date as the user types it — show "no beat on this date" warning
        $('#modal-removed-leads').on('change', '.rl-date', function () {
                var $row = $(this).closest('.rl-row');
                var $status = $row.find('.rl-date-status');
                var d = $(this).val();
                if (!d) {
                        $status.text('');
                        return;
                }
                $status.text('Checking...').css('color', '#94a3b8');
                $.ajax({
                        url: context + '/beatPlan/userBeatsOnDate', type: 'GET', dataType: 'json',
                        data: {authUserId: state.authUserId, date: d},
                        success: function (r) {
                                var dd = r.response || r;
                                var beats = dd.beats || [];
                                if (beats.length === 0) {
                                        $status.text('No beat on this date — pick another date, or Cancel this lead instead.').css('color', '#ef4444');
                                } else {
                                        var names = beats.map(function (b) {
                                                return '"' + b.beatName + '" (day ' + b.dayNumber + ')';
                                        }).join(', ');
                                        $status.text('Will attach to: ' + names).css('color', '#22c55e');
                                }
                        },
                        error: function () {
                                $status.text('Could not verify date').css('color', '#f59e0b');
                        }
                });
        });

        $('#modal-removed-leads').on('click', '#rl-cancel', function () {
                $('#modal-removed-leads').remove();
                // abort save — user goes back to editing
        });

        $('#modal-removed-leads').on('click', '#rl-confirm', function () {
                var actions = [];
                var problems = [];
                $('#modal-removed-leads .rl-row').each(function () {
                        var $row = $(this);
                        var leadId = parseInt($row.data('leadid'));
                        var mode = $row.find('input[type="radio"]:checked').val();
                        if (mode === 'reschedule') {
                                var d = $row.find('.rl-date').val();
                                if (!d) {
                                        problems.push('Lead ' + leadId + ': pick a date or switch to Cancel');
                                        return;
                                }
                                actions.push({leadId: leadId, action: 'reschedule', toDate: d});
                        } else {
                                actions.push({leadId: leadId, action: 'cancel'});
                        }
                });
                if (problems.length) {
                        alert(problems.join('\n'));
                        return;
                }
                $('#modal-removed-leads').remove();
                onConfirm(actions);
        });
}

// ============ 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');
        $('#panel-deferred').toggle(tab === 'deferred');
        $('#cal-grid-container').toggle(tab === 'calendar');
        $('#bottom-bar').toggle(tab === 'route' && (state.mode === 'create' || state.mode === 'edit') && state.days && state.days.length > 0);

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

// Click a beat chip on the calendar → view that specific day's route
// (partners + only the leads scheduled for that exact date)
$(document).on('click', '.cal-chip-view', function (e) {
        if ($(e.target).hasClass('cal-chip-remove')) return; // handled separately
        e.stopPropagation();
        var pg = $(this).data('plangroup');
        var date = $(this).data('date');
        if (!pg || !date) return;

        // Deferred-assign mode: clicking a beat chip drops the deferred item into
        // that beat on that date (instead of opening the route).
        if (state.assignDeferred) {
                assignDeferredToBeat(String(pg), String(date));
                return;
        }

        // Visit-request scheduling mode: similar handoff but talks to /visitRequest/{id}/approve-schedule.
        if (state.assignVisitRequest) {
                approveVisitRequestOnBeat(String(pg), String(date));
                return;
        }

        // Assigned-lead scheduling mode: drop the picked lead onto this beat+date.
        if (state.assignLead) {
                approveLeadOnBeat(String(pg), String(date));
                return;
        }

        // Switch to Route tab and load that day's run
        $('.panel-tab[data-tab="route"]').click();
        viewBeat(String(pg), String(date));
});

// Drop the deferred item (set on entry via URL params) into the chosen beat+date.
function assignDeferredToBeat(beatId, date) {
        var ad = state.assignDeferred;
        if (!ad) return;
        // Must be a FUTURE beat — strictly after the deferred day (and not in the past).
        if (date <= ad.minDate) {
                alert('Pick a later beat. A deferred ' + (ad.type === 'lead' ? 'lead' : 'partner')
                        + ' can only move to a date after ' + ad.minDate + ' — not ' + date + '.');
                return;
        }
        if (!confirm('Add deferred ' + (ad.type === 'lead' ? 'lead' : 'partner') + ' "' + ad.name + '" into this beat on ' + date + '?')) return;
        $.ajax({
                url: context + '/beatPlan/deferred/assignToBeat',
                type: 'POST', contentType: 'application/json',
                data: JSON.stringify({deferredId: ad.id, beatId: parseInt(beatId), date: date}),
                success: function (r) {
                        var d = r.response || r;
                        var label = (ad.type === 'lead' ? 'Lead' : 'Partner') + ' "' + ad.name + '"';
                        var msg = label + ' rescheduled to ' + date + '.';
                        $('#deferred-assign-banner').html('<span style="color:#bbf7d0;">' + msg + '</span>');
                        // Confirm to the head (name included so they don't have to read the banner),
                        // then auto-close the parent's iframe modal. The parent's `hidden.bs.modal`
                        // handler reloads the deferred list (rescheduled row drops off).
                        alert(msg);
                        // Clear the assign-mode so subsequent chip clicks don't try to
                        // re-schedule the same (now-resolved) deferred row and trip the
                        // "already scheduled" guard. Refresh the in-page deferred panel and
                        // switch back to the route tab.
                        state.assignDeferred = null;
                        if (typeof loadDeferredItems === 'function') loadDeferredItems();
                        $('.panel-tab[data-tab="route"]').click();
                        if (window.parent && window.parent !== window) {
                                try {
                                        window.parent.$('#defCalModal').modal('hide');
                                } catch (e) {
                                }
                        }
                },
                error: function (xhr) {
                        var msg = 'Failed';
                        try {
                                var err = JSON.parse(xhr.responseText);
                                if (err && err.response) msg = (typeof err.response === 'string') ? err.response : (err.response.message || msg);
                        } catch (_) {
                        }
                        alert(msg);
                }
        });
}

// ============ 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,
                    planGroupId: b.planGroupId
                                });
                        }
                });
        });

        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 isPast = ds < today;
            var hasBeats = beats.length > 0;

                        var cls = [];
            if (hol && !sun) cls.push('holiday', 'blocked');
            else if (isPast) cls.push('blocked', 'past');
            if (sun && !isPast) cls.push('sunday'); // Sundays styled but not blocked
            if (sun && isPast) cls.push('blocked', 'past');
                        if (isToday) cls.push('today');
                        if (!inMonth) cls.push('blocked');

                        var style = inMonth ? '' : 'opacity:0.3;';
            if (isPast && inMonth) style += 'opacity:0.5;';

            row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" data-hasbeat="' + (hasBeats ? '1' : '0') + '" 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) {
                                // The "running"/[live] state only applies to TODAY's chip — future
                                // chips of the same beat are still freely editable (drag + ×).
                                // Past chips are always locked.
                                var isLiveChip = b.status === 'running' && isToday;
                                var isLockedChip = isPast || isLiveChip;

                                var chipCls = 'cal-chip';
                                if (isLiveChip) chipCls += ' running';
                                if (b.status === 'completed') chipCls += ' completed';
                                var removeBtn = !isLockedChip
                    ? '<span class="cal-chip-remove" data-date="' + ds + '" data-plangroup="' + (b.planGroupId || '') + '" title="Remove">&times;</span>'
                    : '';
                                var draggable = !isLockedChip ? ' draggable="true"' : '';
                                var cursor = draggable ? 'grab' : 'pointer';
                                row += '<span class="' + chipCls + ' cal-chip-view"' + draggable + ' data-plangroup="' + (b.planGroupId || '') + '" data-date="' + ds + '" style="background:' + b.color + ';position:relative;cursor:' + cursor + ';" title="Drag to another date to shuffle">'
                                        + b.name + ' D' + b.day + (isLiveChip ? ' [live]' : '') + ' (' + b.visits + ')'
                    + removeBtn + '</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);
}

// Lists the scheduled dates for a beat (grouped by day number) inside a beat card.
// Output looks like:  "Schedule: D1 May 27 | D2 May 28 | D3 May 29"
// Unscheduled days show as "—"; if NO real dates exist, returns empty string.
function renderBeatScheduledDates(instances) {
        if (!instances || !instances.length) return '';

        // Collect planDates per dayNumber across every instance of this beat
        var byDay = {};
        var today = fmtDate(new Date());
        instances.forEach(function (inst) {
                (inst.days || []).forEach(function (d) {
                        if (!d.planDate) return; // skip unscheduled placeholders
                        if (!byDay[d.dayNumber]) byDay[d.dayNumber] = [];
                        byDay[d.dayNumber].push(d.planDate);
                });
        });

        var dayNums = Object.keys(byDay).map(function (n) {
                return parseInt(n, 10);
        }).sort(function (a, b) {
                return a - b;
        });
        if (dayNums.length === 0) {
                return '<div class="beat-meta" style="color:#475569; margin-top:4px;">Not scheduled yet</div>';
        }

        var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

        function fmt(ds) {
                // ds = "yyyy-mm-dd"
                var p = ds.split('-');
                if (p.length !== 3) return ds;
                return MONTHS[parseInt(p[1], 10) - 1] + ' ' + parseInt(p[2], 10);
        }

        var parts = dayNums.map(function (n) {
                // De-dup + sort dates for this day (could repeat if beat scheduled multiple times)
                var dates = (byDay[n] || []).filter(function (v, i, a) {
                        return a.indexOf(v) === i;
                }).sort();
                var label = dates.map(function (ds) {
                        var color = ds === today ? '#ef4444' : (ds < today ? '#64748b' : '#60a5fa');
                        return '<span style="color:' + color + ';">' + fmt(ds) + '</span>';
                }).join(', ');
                return '<span style="color:#94a3b8;">D' + n + '</span> ' + label;
        });

        return '<div class="beat-meta" style="margin-top:4px; line-height:1.6;">'
                + '<span style="color:#64748b;">Schedule:</span> '
                + parts.join('<span style="color:#334155;"> | </span>')
                + '</div>';
}

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>';
        $('#cal-beat-cards').html(html);
        return;
        }

    // Deduplicate by beatName — group beats with same name, show count
    var beatGroups = {};
        (beats || []).forEach(function (b) {
        var key = b.beatName || b.planGroupId;
        if (!beatGroups[key]) {
            beatGroups[key] = {
                beatName: b.beatName,
                beatColor: b.beatColor,
                instances: []
            };
        }
        beatGroups[key].instances.push(b);
    });

    Object.keys(beatGroups).forEach(function (key) {
        var group = beatGroups[key];
        var first = group.instances[0];
        var scheduledCount = group.instances.filter(function (b) {
            return b.status === 'scheduled';
        }).length;
        var unscheduledCount = group.instances.filter(function (b) {
            return b.status === 'unscheduled';
        }).length;
        var totalInstances = group.instances.length;

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

        // Use first unscheduled instance for scheduling, or first overall
        var primaryPg = first.planGroupId;
        var unscheduledInstance = group.instances.find(function (b) {
            return b.status === 'unscheduled';
        });
        if (unscheduledInstance) primaryPg = unscheduledInstance.planGroupId;

        var statusText = '';
        if (scheduledCount > 0) statusText += scheduledCount + ' scheduled';
        if (unscheduledCount > 0) statusText += (statusText ? ', ' : '') + unscheduledCount + ' unscheduled';

        var isScheduling = state.calSelectedBeat === primaryPg && state.calPickedDates.length > 0;

        html += '<div class="beat-card" data-plangroup="' + primaryPg + '" data-beatname="' + (group.beatName || '') + '" draggable="true">';
        html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + group.beatColor + ';"></span> ' + group.beatName + '</div>';
        html += '<div class="beat-meta">' + (first.totalDays || 1) + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
        html += '<div class="beat-meta" style="color:#94a3b8;">' + statusText + ' (' + totalInstances + ' total)</div>';
                html += renderBeatScheduledDates(group.instances);

        html += '<div class="beat-actions">';
        if (isScheduling) {
            html += '<button class="btn-sm btn-confirm-schedule" data-pg="' + primaryPg + '" data-days="' + (first.totalDays || 1) + '" style="background:#22c55e;color:#fff;">Confirm (' + state.calPickedDates.length + ' dates)</button>';
            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="' + primaryPg + '" data-days="' + (first.totalDays || 1) + '">Schedule</button>';
        }
        html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" onclick="deleteBeat(\'' + primaryPg + '\')">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 style="font-size:9px;color:#475569;margin-top:4px;">Drag onto calendar to schedule</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 during Schedule flow — auto-fill consecutive days from clicked date
$(document).on('click', '#cal-grid-content td', function () {
        if (!state.calSelectedBeat) return;
        var ds = $(this).data('date');
    if (!ds || isDateBlocked(ds)) return;

    // Find the beat to get days needed
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
                return String(b.planGroupId) === String(state.calSelectedBeat);
    });
    var daysNeeded = beat ? (beat.totalDays || 1) : 1;

    // Auto-fill consecutive slots from clicked date
    var slots = findConsecutiveSlots(ds, daysNeeded);
    if (slots) {
        state.calPickedDates = slots;
        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 String(b.planGroupId) === String(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));
                }
        });
});

// ============ DRAG & DROP BEAT TO CALENDAR ============
$(document).on('dragstart', '.beat-card', function (e) {
    var pg = $(this).data('plangroup');
    var beatName = $(this).data('beatname');
    e.originalEvent.dataTransfer.setData('text/plain', pg);
    e.originalEvent.dataTransfer.setData('beatname', beatName || '');
    $(this).css('opacity', '0.5');
});

$(document).on('dragend', '.beat-card', function () {
    $(this).css('opacity', '1');
});

// Calendar cells accept drops
$(document).on('dragover', '#cal-grid-content td', function (e) {
    var ds = $(this).data('date');
    if (!ds) return;
    if (isDateBlocked(ds)) return; // don't allow drop indicator on blocked dates
    e.preventDefault();
    $(this).css('background', '#1e3a5f');
});

$(document).on('dragleave', '#cal-grid-content td', function () {
    $(this).css('background', '');
});

$(document).on('drop', '#cal-grid-content td', function (e) {
    e.preventDefault();
    $(this).css('background', '');

    var pg = e.originalEvent.dataTransfer.getData('text/plain');
    var dropDate = $(this).data('date');
    if (!pg || !dropDate) return;

    if (isDateBlocked(dropDate)) {
        alert('Cannot schedule on this date (past, Sunday, or holiday)');
        return;
    }

    var beat = (state.calData.scheduledBeats || []).find(function (b) {
                return String(b.planGroupId) === String(pg);
    });
    if (!beat) return;
    var beatName = beat.beatName || 'Beat';
    var daysNeeded = beat.totalDays || 1;

    // Auto-fill consecutive available days starting from drop date
    var scheduleDates = findConsecutiveSlots(dropDate, daysNeeded);

    if (!scheduleDates) return; // alerts already shown inside findConsecutiveSlots

    var dateStr = scheduleDates.map(function (d, i) {
        return 'Day ' + (i + 1) + ': ' + d;
    }).join('\n');
    if (!confirm('Schedule "' + beatName + '" (' + daysNeeded + ' days):\n\n' + dateStr + '\n\nConfirm?')) return;

    $.ajax({
        url: context + "/beatPlan/repeatBeat", type: "POST",
        data: {
            sourcePlanGroupId: pg,
            authUserId: state.authUserId,
            dates: JSON.stringify(scheduleDates)
        },
        success: function () {
            loadCalendarData();
        },
        error: function (xhr) {
            alert('Error: ' + (xhr.responseText || xhr.statusText));
        }
    });
});

// Find N consecutive available days starting from startDate, skipping Sundays/holidays
// Returns array of date strings, or null if can't fit
function findConsecutiveSlots(startDateStr, daysNeeded) {
    var startDate = new Date(startDateStr);
    var startMonth = startDate.getMonth();
    var dates = [];
    var d = new Date(startDate);
    var sundayAsked = false;
    var includeSundays = false;

    while (dates.length < daysNeeded) {
        var ds = fmtDate(d);

        // Rule 3: must stay within same month
        if (d.getMonth() !== startMonth) {
            alert('Beat has ' + daysNeeded + ' days but only ' + dates.length + ' available days left in this month starting from ' + startDateStr + '. Try an earlier date.');
            return null;
                }

        // Past dates — skip. Today is skipped too, unless this is an L4+ operator.
        var today = fmtDate(new Date());
        if (ds < today || (ds === today && !state.canScheduleToday)) {
            d.setDate(d.getDate() + 1);
            continue;
        }

        // Holidays — always skip
        if (isHoliday(ds)) {
            d.setDate(d.getDate() + 1);
            continue;
        }

        // Sunday — ask once whether to include
        if (isSunday(ds)) {
            if (!sundayAsked) {
                sundayAsked = true;
                includeSundays = confirm('This beat falls on a Sunday (' + ds + '). Include Sundays in the schedule?');
            }
            if (!includeSundays) {
                d.setDate(d.getDate() + 1);
                continue;
            }
        }

        // Already occupied — block
        if (getBeatsOnDate(ds).length > 0) {
            alert('Cannot schedule: ' + ds + ' already has a beat scheduled. Consecutive days not available from ' + startDateStr + '.');
            return null;
        }

        dates.push(ds);
        d.setDate(d.getDate() + 1);
    }

    return dates;
}

function isDateBlocked(ds) {
    var today = fmtDate(new Date());
    if (ds < today) return true; // past
    if (ds === today && !state.canScheduleToday) return true; // today blocked unless L4+
    // Sundays NOT blocked — handled via confirm dialog instead
    // Only block holidays
    if (state.calData && state.calData.blockedDates) {
        var blocked = state.calData.blockedDates;
        // blockedDates from server includes Sundays+holidays, but we only block holidays now
        var d = new Date(ds);
        if (d.getDay() !== 0) { // not Sunday — check holidays
            if (Array.isArray(blocked) && blocked.indexOf(ds) > -1) return true;
            if (typeof blocked === 'object' && blocked[ds]) return true;
        }
    }
    return false;
}

function isSunday(ds) {
    return new Date(ds).getDay() === 0;
}

function isHoliday(ds) {
    if (!state.calData || !state.calData.holidays) return false;
    return state.calData.holidays.some(function (h) {
        return h.date === ds;
    });
}

function getBeatsOnDate(ds) {
    var result = [];
    if (!state.calData || !state.calData.scheduledBeats) return result;
    state.calData.scheduledBeats.forEach(function (b) {
        (b.days || []).forEach(function (d) {
            if (d.planDate === ds) result.push({planGroupId: b.planGroupId, beatName: b.beatName});
        });
    });
    return result;
}

// ============ REMOVE BEAT FROM CALENDAR DATE ============
$(document).on('click', '.cal-chip-remove', function (e) {
    e.stopPropagation();
    var date = $(this).data('date');
    var planGroupId = $(this).data('plangroup');
    if (!planGroupId || !date) return;

        if (!confirm('Unschedule this beat from ' + date + '?\n\n(The beat stays in the list — only this date is cleared.)')) return;

        // Unschedule ONLY this date — the beat itself is preserved
    $.ajax({
                url: context + "/beatPlan/unscheduleDate", type: "POST",
                data: {planGroupId: String(planGroupId), date: date},
        success: function () {
            loadCalendarData();
        },
        error: function (xhr) {
            alert('Error: ' + (xhr.responseText || xhr.statusText));
        }
        });
});

// ============ CALENDAR SHUFFLE (drag chip to another date) ============
$(document).on('dragstart', '.cal-chip-view', function (e) {
        var pg = $(this).data('plangroup');
        var date = $(this).data('date');
        if (!pg || !date) {
                e.preventDefault();
                return;
        }
        var payload = JSON.stringify({pg: String(pg), fromDate: String(date)});
        var dt = e.originalEvent.dataTransfer;
        dt.effectAllowed = 'move';
        dt.setData('text/plain', payload);
        $(this).css('opacity', '0.4');
});

$(document).on('dragend', '.cal-chip-view', function () {
        $(this).css('opacity', '');
});

$(document).on('dragover', '.cal-grid td', function (e) {
        var $td = $(this);
        // Today is the live/running slot — nothing can be shuffled onto it. The
        // shuffle is only valid between future dates.
        if ($td.hasClass('past') || $td.hasClass('blocked') || $td.hasClass('holiday') || $td.hasClass('sunday') || $td.hasClass('today')) return;
        e.preventDefault();
        e.originalEvent.dataTransfer.dropEffect = 'move';
        $td.css('box-shadow', 'inset 0 0 0 2px #60a5fa');
});

$(document).on('dragleave', '.cal-grid td', function () {
        $(this).css('box-shadow', '');
});

$(document).on('drop', '.cal-grid td', function (e) {
        var $td = $(this);
        $td.css('box-shadow', '');
        if ($td.hasClass('past') || $td.hasClass('blocked') || $td.hasClass('holiday') || $td.hasClass('sunday') || $td.hasClass('today')) return;
        e.preventDefault();

        var raw;
        try {
                raw = e.originalEvent.dataTransfer.getData('text/plain');
        } catch (_) {
                return;
        }
        if (!raw) return;
        var data;
        try {
                data = JSON.parse(raw);
        } catch (_) {
                return;
        }
        if (!data.pg || !data.fromDate) return;

        var toDate = $td.data('date');
        if (!toDate || toDate === data.fromDate) return;

        $.ajax({
                url: context + "/beatPlan/moveScheduleDate", type: "POST",
                data: {planGroupId: data.pg, fromDate: data.fromDate, toDate: String(toDate)},
                success: function () {
                        loadCalendarData();
                },
                error: function (xhr) {
                        var msg = 'Move failed';
                        try {
                                var err = JSON.parse(xhr.responseText);
                                if (err && err.response) {
                                        if (typeof err.response === 'string') msg = err.response;
                                        else if (err.response.message) msg = err.response.message;
                                }
                        } catch (_) {
                        }
                        alert(msg);
                }
        });
});

// ============ 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() {
        var b = new google.maps.LatLngBounds();
        var any = false;
        if (homeMarker) {
                b.extend(homeMarker.getPosition());
                any = true;
        }

        // Prefer the current route's stops — the unselected partners on the map can
        // span a much wider area than the actual planned visits, which would zoom
        // the camera all the way out. We want focus on the route the user is viewing.
        try {
                var day = curDay();
                if (day && day.visits && day.visits.length > 0) {
                        day.visits.forEach(function (v) {
                                if (v.lat && v.lng) {
                                        b.extend(new google.maps.LatLng(parseFloat(v.lat), parseFloat(v.lng)));
                                        any = true;
                                }
                        });
                }
        } catch (e) {
        }

        // No route yet (e.g., still picking partners) → fit all rendered markers.
        if (!any) {
                for (var k in markers) {
                        b.extend(markers[k].getPosition());
                        any = true;
                }
        }
        if (any) map.fitBounds(b);
}

// ============ AUTO-LOAD FROM URL PARAMS ============
// When opened with ?autoUserId=X[&autoUserName=Y], skip the user-picker
// dropdowns and load that user's beats directly (and jump to Calendar tab).
$(function () {
        // Use a manual parser so we don't depend on URLSearchParams
        function getParam(name) {
                var q = window.location.search.substring(1);
                var pairs = q.split('&');
                for (var i = 0; i < pairs.length; i++) {
                        var kv = pairs[i].split('=');
                        if (decodeURIComponent(kv[0]) === name) {
                                return kv.length > 1 ? decodeURIComponent(kv[1].replace(/\+/g, ' ')) : '';
                        }
                }
                return null;
        }

        var autoUid = getParam('autoUserId');
        console.log('[BEAT-AUTO] autoUserId =', autoUid);
        if (!autoUid) return;

        var autoName = getParam('autoUserName');
        var editBeatId = getParam('editBeatId');
        var editDate = getParam('editDate');

        // Deferred-assign mode (opened from the Deferred Partners panel): the head
        // picks an upcoming beat on the calendar to drop this deferred item into.
        var assignDeferredId = getParam('assignDeferredId');
        if (assignDeferredId) {
                var d2 = new Date();
                var todayStr = d2.getFullYear() + '-' + ('0' + (d2.getMonth() + 1)).slice(-2) + '-' + ('0' + d2.getDate()).slice(-2);
                var deferDate = getParam('deferDate') || '';
                // A deferral can only move FORWARD — never to the day it was deferred or
                // earlier. The floor is the later of (deferred date, today).
                var floor = (deferDate && deferDate > todayStr) ? deferDate : todayStr;
                state.assignDeferred = {
                        id: parseInt(assignDeferredId),
                        type: getParam('deferType') || 'visit',
                        name: getParam('deferName') || 'item',
                        deferDate: deferDate,
                        minDate: floor // target date must be strictly AFTER this
                };
        }

        state.authUserId = parseInt(autoUid);
        state.categoryId = parseInt($('#bp-category').val()) || 4;
        state.mode = 'list';
        $('#bp-user-label').text(autoName || ('User #' + autoUid));

        $.ajax({
                url: context + '/beatPlan/getBaseLocation', type: 'GET', dataType: 'json',
                data: {authUserId: autoUid},
                success: function (r) {
                        console.log('[BEAT-AUTO] getBaseLocation ok');
                        var data = r.response || r;
                        if (data.locationName) {
                                state.homeLat = parseFloat(data.latitude);
                                state.homeLng = parseFloat(data.longitude);
                                state.homeName = data.locationName;
                        }
                        if (editBeatId) {
                                console.log('[BEAT-AUTO] entering edit for beat', editBeatId, 'date', editDate);
                                editBeat(String(editBeatId), editDate || null);
                        } else {
                                showBeatList();
                                setTimeout(function () {
                                        $('.panel-tab[data-tab="calendar"]').click();
                                }, 100);
                                if (state.assignDeferred) {
                                        // Banner instructing the head to click an upcoming beat.
                                        var ad = state.assignDeferred;
                                        $('#deferred-assign-banner').remove();
                                        $('body').prepend(
                                                '<div id="deferred-assign-banner" style="position:sticky;top:0;z-index:2000;'
                                                + 'background:#0d9488;color:#fff;padding:8px 16px;font-size:13px;">'
                                                + 'Adding deferred ' + (ad.type === 'lead' ? 'lead' : 'partner') + ': '
                                                + '<strong>' + ad.name + '</strong> — click an upcoming beat on the calendar to drop it in.'
                                                + '</div>');
                                }
                        }
                },
                error: function (xhr) {
                        console.error('[BEAT-AUTO] getBaseLocation failed:', xhr.status, xhr.responseText);
                }
        });
});

// ============ VISIT REQUEST PANEL ============
// Lives on the route tab (sidebar) when a sales user is loaded. Fetches
// PENDING requests addressed at this user's hierarchy and shows three actions
// per row: Schedule on a beat, Reassign, Reject. Schedule reuses the calendar
// chip-click flow by setting state.assignVisitRequest = {…} and switching
// the panel to the Calendar tab — clicking a future chip then fires the POST.
function loadVisitRequests() {
        if (!state.authUserId) return;
        $.ajax({
                url: context + '/visitRequest/list',
                type: 'GET', dataType: 'json',
                // Force a fresh fetch — otherwise the browser HTTP cache can serve the
                // pre-reassign list and the panel won't reflect the new ownership.
                cache: false,
                data: {assigneeAuthId: state.authUserId, status: 'PENDING'},
                success: function (r) {
                        var data = r.response || r;
                        var rows = (data && data.rows) || [];
                        state.visitRequests = rows;
                        renderVisitRequests(rows);
                },
                error: function () {
                        $('#visit-request-panel').hide();
                }
        });
}

function renderVisitRequests(rows) {
        if (!rows || rows.length === 0) {
                $('#visit-request-panel').hide();
                return;
        }
        $('#vr-count').text('(' + rows.length + ')');
        var html = '';
        rows.forEach(function (r) {
                var safeName = String(r.leadName || '').replace(/"/g, '&quot;');
                // Line 1: outlet · mobile · city, state
                var top = [];
                if (r.leadOutlet) top.push(r.leadOutlet);
                if (r.leadMobile) top.push(r.leadMobile);
                var loc = [r.leadCity, r.leadState].filter(Boolean).join(', ');
                if (loc) top.push(loc);
                var topLine = top.length ? '<div class="beat-meta">' + top.join(' · ') + '</div>' : '';
                // Line 2: nearest store (only when known)
                var nearLine = r.nearestStoreCode
                        ? '<div class="beat-meta">Near ' + r.nearestStoreCode
                                + (r.nearestStoreOutlet ? ' — ' + r.nearestStoreOutlet : '') + '</div>'
                        : '';
                // Line 3: by requester · Pref date. Assignee + communication type are intentionally
                // not shown here (assignee id still travels on the button data for the Reassign prompt).
                var reqBits = [];
                if (r.requestedByName) reqBits.push('by ' + r.requestedByName + (r.requestedByAuthId ? ' (#' + r.requestedByAuthId + ')' : ''));
                if (r.requestedDate) reqBits.push('Pref <span style="color:#fbbf24;">' + r.requestedDate + '</span>');
                var reqLine = reqBits.length ? '<div class="beat-meta">' + reqBits.join(' · ') + '</div>' : '';
                html += '<div class="beat-card" data-rid="' + r.id + '" style="cursor:default;">'
                        + '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;flex:none;background:#fbbf24;"></span> ' + (r.leadName || ('Lead #' + r.leadId)) + '</div>'
                        + topLine
                        + nearLine
                        + reqLine
                        + '<div class="beat-actions" style="flex-wrap:wrap;">'
                        + '<button class="vr-schedule btn-sm" data-rid="' + r.id + '" data-lead="' + r.leadId + '" data-leadname="' + safeName + '" data-assignee="' + r.assigneeAuthId + '" style="background:#22c55e;color:#fff;">Schedule</button>'
                        + '<button class="vr-reassign btn-sm" data-rid="' + r.id + '" style="background:none;border:1px solid #475569;color:#cbd5e1;">Reassign</button>'
                        + '<button class="vr-reject btn-sm" data-rid="' + r.id + '" style="background:#ef4444;color:#fff;">Reject</button>'
                        + '</div>'
                        + '</div>';
        });
        $('#vr-list').html(html);
        $('#visit-request-panel').show();
}

$(document).on('click', '#vr-refresh', function () {
        loadVisitRequests();
});

// Collapse / expand the visit-requests list (accordion header).
$(document).on('click', '#vr-toggle', function () {
        var open = $('#vr-body').is(':visible');
        $('#vr-body').slideToggle(120);
        $('#vr-caret').html(open ? '&#9656;' : '&#9662;');
});

// Enter "schedule a visit request" mode — same handoff to the calendar chip
// click that the deferred-assign flow uses.
$(document).on('click', '.vr-schedule', function () {
        var $b = $(this);
        state.assignVisitRequest = {
                id: parseInt($b.data('rid')),
                leadId: parseInt($b.data('lead')),
                leadName: String($b.data('leadname') || ''),
                assigneeAuthId: parseInt($b.data('assignee'))
        };
        alert('Click a future beat-chip on the calendar to schedule "'
                + (state.assignVisitRequest.leadName || ('Lead #' + state.assignVisitRequest.leadId))
                + '" onto it.');
        $('.panel-tab[data-tab="calendar"]').click();
});

// "+ New beat" — open the full map-first planner, same as the big "+ New Beat"
// button (no date prompt / no auto 1-day beat).
$(document).on('click', '.vr-new-beat', function () {
        startNewBeat();
});

$(document).on('click', '.vr-reassign', function () {
        var rid = $(this).data('rid');
        var newId = prompt('New assignee auth user id (must be in your downline):');
        if (!newId) return;
        var newIdInt = parseInt(newId);
        if (!newIdInt) return;
        $.ajax({
                url: context + '/visitRequest/' + rid + '/reassign',
                type: 'POST', contentType: 'application/json',
                data: JSON.stringify({newAssigneeAuthId: newIdInt}),
                success: function () {
                        alert('Reassigned to user #' + newIdInt + '. The request will drop off this panel.');
                        loadVisitRequests();
                },
                error: function (xhr) {
                        var msg = 'Reassign failed';
                        try {
                                var e = JSON.parse(xhr.responseText);
                                if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
                        } catch (_) {
                        }
                        alert(msg);
                }
        });
});

$(document).on('click', '.vr-reject', function () {
        var rid = $(this).data('rid');
        var reason = prompt('Reject reason (required):');
        if (!reason || !reason.trim()) return;
        $.ajax({
                url: context + '/visitRequest/' + rid + '/reject',
                type: 'POST', contentType: 'application/json',
                data: JSON.stringify({rejectReason: reason.trim()}),
                success: function () {
                        alert('Request rejected. It will drop off this panel.');
                        loadVisitRequests();
                },
                error: function (xhr) {
                        var msg = 'Reject failed';
                        try {
                                var e = JSON.parse(xhr.responseText);
                                if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
                        } catch (_) {
                        }
                        alert(msg);
                }
        });
});

// ============ ASSIGNED LEADS PANEL ============
// Lives on the route tab (sidebar) below Pending Visit Requests. Lists the
// loaded user's active/open leads (pending + follow-up, yellow/green). Each row
// offers "Schedule on a beat" (reuses the calendar-chip handoff, like visit
// requests) and "New beat" (a 1-day beat with just this lead). Hidden when empty.
function loadAssignedLeads() {
        if (!state.authUserId) return;
        $.ajax({
                url: context + '/beatPlan/assignedLeads',
                type: 'GET', dataType: 'json',
                cache: false,
                data: {authUserId: state.authUserId},
                success: function (r) {
                        var data = r.response || r;
                        var rows = (data && data.rows) || [];
                        state.assignedLeads = rows;
                        renderAssignedLeads(rows);
                },
                error: function () {
                        $('#assigned-leads-panel').hide();
                }
        });
}

function renderAssignedLeads(rows) {
        if (!rows || rows.length === 0) {
                $('#assigned-leads-panel').hide();
                return;
        }
        $('#al-count').text('(' + rows.length + ')');
        $('#al-search').val('');
        $('#al-list').html(alRowsHtml(rows));
        $('#assigned-leads-panel').show();
}

// Lead rows reuse the beat-card look (.beat-card/.beat-name/.beat-meta/.beat-actions)
// so leads and routes read as one system. The colour dot mirrors the beat colour
// swatch — here it's the lead's yellow/green priority colour.
function alRowsHtml(rows) {
        if (!rows || rows.length === 0) {
                return '<div style="padding:12px;color:#64748b;font-size:11px;text-align:center;">No matching leads.</div>';
        }
        var html = '';
        rows.forEach(function (r) {
                var safeName = String(r.leadName || '').replace(/"/g, '&quot;');
                var dot = r.color === 'green' ? '#22c55e' : (r.color === 'yellow' ? '#eab308' : '#64748b');
                var metaParts = [];
                if (r.outletName) metaParts.push(r.outletName);
                if (r.mobile) metaParts.push(r.mobile);
                var loc = [r.city, r.state].filter(Boolean).join(', ');
                if (loc) metaParts.push(loc);
                if (r.stage) metaParts.push(r.stage);
                var meta = metaParts.join(' · ')
                        + (r.hasGeo ? '' : ' <span style="color:#f59e0b;" title="No approved location — the field rep won\'t get a map pin">· no location</span>');
                html += '<div class="beat-card" data-lid="' + r.leadId + '" style="cursor:default;">'
                        + '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;flex:none;background:' + dot + ';"></span> ' + (r.leadName || ('Lead #' + r.leadId)) + '</div>'
                        + '<div class="beat-meta">' + meta + '</div>'
                        + '<div class="beat-actions">'
                        + '<button class="al-schedule btn-sm" data-lead="' + r.leadId + '" data-leadname="' + safeName + '" style="background:#22c55e;color:#fff;">Schedule on a beat</button>'
                        + '<button class="al-new-beat btn-sm" data-lead="' + r.leadId + '" data-leadname="' + safeName + '" style="background:#3b82f6;color:#fff;" title="Use this when the user has no beat to land on">+ New beat</button>'
                        + '</div>'
                        + '</div>';
        });
        return html;
}

$(document).on('click', '#al-refresh', function () {
        loadAssignedLeads();
});

// Collapse / expand the assigned-leads list (accordion header).
$(document).on('click', '#al-toggle', function () {
        var open = $('#al-body').is(':visible');
        $('#al-body').slideToggle(120);
        $('#al-caret').html(open ? '&#9656;' : '&#9662;');
});

// Client-side filter over the loaded leads — keeps the panel short without a round-trip.
$(document).on('input', '#al-search', function () {
        var q = $(this).val().toLowerCase().trim();
        var src = state.assignedLeads || [];
        var list = !q ? src : src.filter(function (p) {
                return (p.leadName || '').toLowerCase().indexOf(q) !== -1
                        || (p.outletName || '').toLowerCase().indexOf(q) !== -1
                        || (p.city || '').toLowerCase().indexOf(q) !== -1
                        || (p.mobile || '').toLowerCase().indexOf(q) !== -1;
        });
        $('#al-list').html(alRowsHtml(list));
});

// Enter "schedule a lead" mode — same calendar-chip handoff as visit requests.
$(document).on('click', '.al-schedule', function () {
        var $b = $(this);
        state.assignLead = {
                leadId: parseInt($b.data('lead')),
                leadName: String($b.data('leadname') || '')
        };
        alert('Click a future beat-chip on the calendar to schedule "'
                + (state.assignLead.leadName || ('Lead #' + state.assignLead.leadId))
                + '" onto it.');
        $('.panel-tab[data-tab="calendar"]').click();
});

// "+ New beat" — open the full map-first planner, same as the big "+ New Beat"
// button (no date prompt / no auto 1-day beat).
$(document).on('click', '.al-new-beat', function () {
        startNewBeat();
});

// Drop the picked lead onto the chosen beat+date (strictly-future chip).
function approveLeadOnBeat(pg, date) {
        var al = state.assignLead;
        if (!al) return;
        var today = new Date();
        var todayStr = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2) + '-' + ('0' + today.getDate()).slice(-2);
        if (date <= todayStr) {
                alert('Pick a FUTURE date (after today).');
                return;
        }
        if (!confirm('Schedule "' + (al.leadName || ('Lead #' + al.leadId)) + '" onto this beat on ' + date + '?')) return;
        $.ajax({
                url: context + '/beatPlan/lead/scheduleOnBeat',
                type: 'POST', contentType: 'application/json',
                data: JSON.stringify({leadId: al.leadId, beatId: parseInt(pg), date: date}),
                success: function (resp) {
                        var r = resp && resp.response ? resp.response : resp;
                        state.assignLead = null;
                        alert(r.message || 'Scheduled.');
                        $('.panel-tab[data-tab="route"]').click();
                        loadAssignedLeads();
                },
                error: function (xhr) {
                        var msg = 'Schedule failed';
                        try {
                                var er = JSON.parse(xhr.responseText);
                                if (er && er.response) msg = (typeof er.response === 'string') ? er.response : (er.response.message || msg);
                        } catch (_) {
                        }
                        alert(msg);
                }
        });
}

// ============ DEFERRED ITEMS PANEL (inside beat-plan-window) ============
// Reuses the existing /beatPlan/deferred read (hierarchy-scoped) and filters
// client-side to the currently-loaded sales user. Schedule → switches to the
// Calendar tab in deferred-assign mode and reuses assignDeferredToBeat() on
// chip click (existing flow). Cancel posts /beatPlan/deferred/action with a
// required reason. The standalone Deferred Partners page is untouched.
function loadDeferredItems() {
        if (!state.authUserId) return;
        // Wide window so recent misses + any not-yet-acted older ones surface.
        var now = new Date();

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

        var start = new Date();
        start.setDate(now.getDate() - 30);
        var end = new Date();
        end.setDate(now.getDate() + 30);
        $.ajax({
                url: context + '/beatPlan/deferred',
                type: 'GET', dataType: 'json', cache: false,
                data: {startDate: fmt(start), endDate: fmt(end)},
                success: function (r) {
                        var data = r.response || r;
                        var all = (data && data.rows) || [];
                        var mine = all.filter(function (row) {
                                return row.authUserId === state.authUserId;
                        });
                        state.deferredItems = mine;
                        renderDeferredItems(mine);
                },
                error: function () {
                        $('#di-list').html('<p style="color:#d9534f;font-size:12px;text-align:center;padding:20px 10px;">Could not load deferred items.</p>');
                }
        });
}

function renderDeferredItems(rows) {
        var n = (rows && rows.length) || 0;
        $('#di-count').text('(' + n + ')');
        // Tab pill counter — keeps the head aware of pending items while on other tabs.
        if (n > 0) {
                $('#di-tab-count').text('(' + n + ')').css('color', '#fca5a5');
        } else {
                $('#di-tab-count').text('').css('color', '');
        }
        if (n === 0) {
                $('#di-list').html('<p style="color:#64748b;font-size:12px;text-align:center;padding:20px 10px;">No deferred items for this user.</p>');
                return;
        }
        var html = '';
        rows.forEach(function (r) {
                var safeName = String(r.name || '').replace(/"/g, '&quot;');
                var typeTag = r.type === 'Lead'
                        ? '<span style="background:#fbbf24;color:#0f172a;font-size:10px;padding:1px 5px;border-radius:3px;margin-right:6px;">Lead</span>'
                        : '<span style="background:#60a5fa;color:#0f172a;font-size:10px;padding:1px 5px;border-radius:3px;margin-right:6px;">Visit</span>';
                var nextLine = r.nextScheduledDate
                        ? '<div style="color:#22c55e;font-size:11px;">Auto-covered on ' + r.nextScheduledDate + '</div>' : '';
                html += '<div data-did="' + r.id + '" style="padding:8px;border:1px solid #334155;border-radius:6px;background:#0f172a;margin-bottom:6px;">'
                        + typeTag
                        + '<strong style="color:#e2e8f0;">' + (r.name || '#' + r.fofoStoreId) + '</strong>'
                        + '<div style="color:#94a3b8;font-size:11px;margin-top:2px;">'
                        + 'Deferred: <span style="color:#fca5a5;">' + (r.deferredDate || '-') + '</span>'
                        + (r.reason ? ' · ' + r.reason : '')
                        + '</div>'
                        + nextLine
                        + '<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;">'
                        + '<button class="di-schedule" data-did="' + r.id + '" data-type="' + (r.type === 'Lead' ? 'lead' : 'visit') + '" data-name="' + safeName + '" data-deferdate="' + (r.deferredDate || '') + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#22c55e;color:#fff;cursor:pointer;">Schedule on a beat</button>'
                        + '<button class="di-cancel" data-did="' + r.id + '" data-name="' + safeName + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#ef4444;color:#fff;cursor:pointer;">Cancel</button>'
                        + '</div>'
                        + '</div>';
        });
        $('#di-list').html(html);
}

$(document).on('click', '#di-refresh', function () {
        loadDeferredItems();
});

// Schedule: set state.assignDeferred (existing pattern) and switch to the
// Calendar tab. The existing .cal-chip-view handler dispatches into
// assignDeferredToBeat() when state.assignDeferred is set.
$(document).on('click', '.di-schedule', function () {
        var $b = $(this);
        var deferDate = String($b.data('deferdate') || '');
        var todayObj = new Date();
        var todayStr = todayObj.getFullYear() + '-' + ('0' + (todayObj.getMonth() + 1)).slice(-2) + '-' + ('0' + todayObj.getDate()).slice(-2);
        // Floor: later of the deferred-date OR today — same guard the iframe flow uses.
        var floor = (deferDate && deferDate > todayStr) ? deferDate : todayStr;
        state.assignDeferred = {
                id: parseInt($b.data('did')),
                type: String($b.data('type') || 'visit'),
                name: String($b.data('name') || 'item'),
                deferDate: deferDate,
                minDate: floor
        };
        alert('Click a future beat-chip on the calendar to drop "' + (state.assignDeferred.name) + '" onto it (after ' + floor + ').');
        $('.panel-tab[data-tab="calendar"]').click();
});

$(document).on('click', '.di-cancel', function () {
        var did = $(this).data('did');
        var name = String($(this).data('name') || 'this deferred item');
        var reason = prompt('Cancel reason (required) — why is "' + name + '" being cancelled?');
        if (!reason || !reason.trim()) return;
        $.ajax({
                url: context + '/beatPlan/deferred/action',
                type: 'POST', contentType: 'application/json',
                data: JSON.stringify({deferredId: did, action: 'cancel', reason: reason.trim()}),
                success: function () {
                        alert('Cancelled. The row will drop off this panel.');
                        loadDeferredItems();
                },
                error: function (xhr) {
                        var msg = 'Cancel failed';
                        try {
                                var e = JSON.parse(xhr.responseText);
                                if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
                        } catch (_) {
                        }
                        alert(msg);
                }
        });
});

function approveVisitRequestOnBeat(pg, date) {
        var vr = state.assignVisitRequest;
        if (!vr) return;
        var today = new Date();
        var todayStr = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2) + '-' + ('0' + today.getDate()).slice(-2);
        if (date <= todayStr) {
                alert('Pick a FUTURE date (after today).');
                return;
        }
        if (!confirm('Schedule "' + (vr.leadName || ('Lead #' + vr.leadId)) + '" onto this beat on ' + date + '?')) return;
        $.ajax({
                url: context + '/visitRequest/' + vr.id + '/approve-schedule',
                type: 'POST', contentType: 'application/json',
                data: JSON.stringify({beatId: parseInt(pg), scheduleDate: date}),
                success: function () {
                        state.assignVisitRequest = null;
                        alert('Scheduled. The request has been resolved.');
                        $('.panel-tab[data-tab="route"]').click();
                        loadVisitRequests();
                },
                error: function (xhr) {
                        var msg = 'Schedule failed';
                        try {
                                var er = JSON.parse(xhr.responseText);
                                if (er && er.response) msg = (typeof er.response === 'string') ? er.response : (er.response.message || msg);
                        } catch (_) {
                        }
                        alert(msg);
                }
        });
}