/**
 * Yatoon Staff Portal - Frontend JS
 * PIN login -> schedule view -> booking detail sheet
 */
(function () {
    'use strict';

    /* State */
    const state = {
        staffId:   null,
        staffName: null,
        staffRole: 'staff',
        portalToken: '',
        pin:       '',
        tab:       0,          // index into weekDays
        weekOffset: 0,          // 0 = current week, -1 = previous 7 days, +1 = next 7 days
        schedule:  {},         // { 'YYYY-MM-DD': [booking, ...] }
        clients:   [],
        staffRoster: [],
        weekDays:  [],         // ['YYYY-MM-DD', ...] 7 days for the selected portal week
        activeBooking: null,
        activeClient: null,
        messageBookingId: 0,
        currentView: 'today',
        calendarMode: 'list',
        hasSavedCalendarMode: false,
        monthAnchor: (function () { const d = new Date(); d.setDate(1); d.setHours(0,0,0,0); return d.toISOString().slice(0, 10); })(),
        selectedDate: (function () { const d = new Date(); d.setHours(0,0,0,0); return d.toISOString().slice(0, 10); })(),
        selectedStaffIds: null,
        staffFilterInitialized: false,
        pollTimer: null,       // auto-refresh interval
        loginPending: false,
        permissions: {
            view_schedule: true,
            view_all_schedules: false,
            update_status: false,
            create_booking: false,
            edit_booking: false,
            cancel_booking: false,
            view_clients: false,
            manage_clients: false,
            send_messages: false,
            checkout: false,
            request_time_off: false,
            approve_time_off: false,
        },
    };

    const POLL_INTERVAL = 60000; // 60 seconds

    const PIN_LEN = Math.max(4, Math.min(6, parseInt((window.yspData && window.yspData.pinLength) || 4, 10) || 4));

    /* DOM shortcuts */
    const $ = id => document.getElementById(id);
    const qsa = sel => document.querySelectorAll(sel);

    /* Screens */
    function showScreen(id) {
        document.querySelectorAll('.ysp-screen').forEach(el => el.classList.remove('active'));
        document.getElementById(id).classList.add('active');
    }

    /* Toast */
    function toast(msg, duration = 2200) {
        const t = $('ysp-toast');
        t.textContent = msg;
        t.classList.add('show');
        setTimeout(() => t.classList.remove('show'), duration);
    }

    function showClientAccessDenied() {
        const msg = 'You do not have permission to view client profiles.';
        toast(msg, 3200);
        window.setTimeout(() => {
            if (document.hidden) return;
            window.alert(msg);
        }, 50);
    }

    function checkoutPayUrl() {
        return (yspData.squareCheckoutUrl || yspData.paymentRequestUrl || '').trim();
    }

    function squareNoShowFeeEnabled() {
        return yspData.activePlatform === 'square' && !!yspData.squareNoShowFeeEnabled;
    }

    function squareNoShowFeeAmount() {
        const amount = parseFloat(yspData.squareNoShowFeeAmount || 0);
        return Math.max(0, Number.isFinite(amount) ? amount : 0).toFixed(2);
    }

    function copyText(text) {
        if (!text) {
            toast('No payment link configured');
            return;
        }
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(text).then(() => toast('Copied')).catch(() => fallbackCopyText(text));
            return;
        }
        fallbackCopyText(text);
    }

    function fallbackCopyText(text) {
        const input = document.createElement('textarea');
        input.value = text;
        input.setAttribute('readonly', 'readonly');
        input.style.position = 'fixed';
        input.style.left = '-9999px';
        document.body.appendChild(input);
        input.select();
        try {
            document.execCommand('copy');
            toast('Copied');
        } catch (e) {
            toast('Copy failed');
        }
        document.body.removeChild(input);
    }

    function updatePermissionUI() {
        const addBtn = $('ysp-add-booking-btn');
        if (addBtn) addBtn.style.display = state.permissions.create_booking ? '' : 'none';
        const bottomAddBtn = $('ysp-bottom-add-btn');
        if (bottomAddBtn) bottomAddBtn.style.display = 'none';
        const clientsBtn = $('ysp-bottom-clients-btn');
        if (clientsBtn) clientsBtn.style.display = state.permissions.view_clients ? '' : 'none';
        const messagesBtn = $('ysp-bottom-messages-btn');
        if (messagesBtn) messagesBtn.style.display = state.permissions.send_messages ? '' : 'none';
        const checkoutBtn = $('ysp-bottom-checkout-btn');
        if (checkoutBtn) checkoutBtn.style.display = state.permissions.checkout ? '' : 'none';
    }

    function updateCalendarControls() {
        document.querySelectorAll('[data-calendar-mode]').forEach(btn => {
            const active = btn.dataset.calendarMode === state.calendarMode;
            btn.classList.toggle('active', active);
            btn.setAttribute('aria-pressed', active ? 'true' : 'false');
        });
        const toolbar = $('ysp-calendar-toolbar');
        if (toolbar) toolbar.style.display = state.currentView === 'calendar' ? '' : 'none';
    }

    function setCalendarMode(mode) {
        if (!['day', 'week', 'list', 'month'].includes(mode)) return;
        const wasMonth = state.calendarMode === 'month';
        state.calendarMode = mode;
        state.hasSavedCalendarMode = true;
        try {
            localStorage.setItem('ysp_calendar_mode', mode);
        } catch (e) {}
        updateCalendarControls();
        if (wasMonth !== (mode === 'month')) {
            // Data range differs between month view and week view - refetch.
            loadSchedule(false);
            return;
        }
        renderSchedule();
    }

    /* Week days */
    function buildWeekDays() {
        state.weekDays = [];
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        today.setDate(today.getDate() + (state.weekOffset * 7));
        for (let i = 0; i < 7; i++) {
            const d = new Date(today);
            d.setDate(today.getDate() + i);
            state.weekDays.push(d.toISOString().slice(0, 10));
        }
    }

    /* Month grid (simplified calendar view) */
    function monthGridInfo(anchorIso) {
        const parts = anchorIso.split('-');
        const year = parseInt(parts[0], 10);
        const month = parseInt(parts[1], 10) - 1;
        const firstOfMonth = new Date(year, month, 1);
        const lastOfMonth = new Date(year, month + 1, 0);
        const gridStart = new Date(firstOfMonth);
        gridStart.setDate(gridStart.getDate() - gridStart.getDay()); // back up to Sunday
        const cells = [];
        for (let i = 0; i < 42; i++) {
            const d = new Date(gridStart);
            d.setDate(gridStart.getDate() + i);
            cells.push({
                iso: d.toISOString().slice(0, 10),
                inMonth: d.getMonth() === month,
            });
        }
        return {
            cells: cells,
            // Data is only fetched for the actual month (stays within the
            // server's 30-day range cap); leading/trailing pad days from
            // neighboring months are shown dimmed with no dot data.
            monthStart: firstOfMonth.toISOString().slice(0, 10),
            monthEnd: lastOfMonth.toISOString().slice(0, 10),
            label: firstOfMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }),
        };
    }

    function changeMonth(delta) {
        const parts = state.monthAnchor.split('-');
        const d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1 + delta, 1);
        state.monthAnchor = d.toISOString().slice(0, 10);
        loadSchedule(false);
    }

    function formatDate(iso) {
        // iso is always a 'YYYY-MM-DD' string for day tab grouping
        const parts = iso.split('-');
        const d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
        return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
    }

    function formatDayTab(iso) {
        const parts = iso.split('-');
        const d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
        return {
            day: d.toLocaleDateString('en-US', { weekday: 'short' }),
            num: d.getDate(),
        };
    }

    function formatTime(val) {
        // val can be a Unix timestamp in ms (number) or an ISO string
        const d = (typeof val === 'number') ? new Date(val) : new Date(val);
        return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
    }

    function formatDateValue(val) {
        if (!val) return '-';
        if (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}/.test(val)) {
            return formatDate(val.slice(0, 10));
        }
        const d = new Date(val);
        if (Number.isNaN(d.getTime())) return '-';
        return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
    }

    function formatHour(hour) {
        const d = new Date();
        d.setHours(hour, 0, 0, 0);
        return d.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true });
    }

    function isoForSelectedDay(hour, minute = 0) {
        const iso = state.weekDays[state.tab];
        const parts = iso.split('-');
        const d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), hour, minute, 0, 0);
        return d;
    }

    /* PIN numpad */
    function updatePinDisplay() {
        document.querySelectorAll('.ysp-pin-dot').forEach((dot, i) => {
            dot.classList.toggle('filled', i < state.pin.length);
        });
    }

    function handleDigit(digit) {
        if (state.loginPending) return;
        if (state.pin.length >= PIN_LEN) return;
        state.pin += digit;
        updatePinDisplay();
        if (state.pin.length === PIN_LEN) submitPin();
    }

    function handleBackspace() {
        if (state.loginPending) return;
        state.pin = state.pin.slice(0, -1);
        updatePinDisplay();
        $('ysp-login-error').textContent = '';
    }

    function staffDeviceId() {
        const key = 'ysp_staff_device_id';
        try {
            let id = localStorage.getItem(key);
            if (!id) {
                id = 'ysp-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 12);
                localStorage.setItem(key, id);
            }
            return id;
        } catch (e) {
            return 'session-' + Date.now().toString(36);
        }
    }

    function applyDefaultCalendarModeForRole() {
        // Staff who only see their own schedule (not the whole team's) get the
        // simplified Month view by default, unless they've already picked a mode.
        if (!state.hasSavedCalendarMode && !state.permissions.view_all_schedules) {
            state.calendarMode = 'month';
        }
    }

    function submitPin() {
        if (state.loginPending) return;
        state.loginPending = true;
        $('ysp-login-error').textContent = '';
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_portal_login',
            nonce:  yspData.nonce,
            pin:    state.pin,
            device_id: staffDeviceId(),
        }, function (resp) {
            if (resp.success) {
                state.staffId   = resp.data.staff_id;
                state.staffName = resp.data.name;
                state.staffRole = resp.data.role || 'staff';
                state.portalToken = resp.data.portal_token || '';
                state.permissions = Object.assign({}, state.permissions, resp.data.permissions || {});
                updatePermissionUI();
                applyDefaultCalendarModeForRole();
                sessionStorage.setItem('ysp_staff_id',   state.staffId);
                sessionStorage.setItem('ysp_staff_name', state.staffName);
                sessionStorage.setItem('ysp_staff_role', state.staffRole);
                sessionStorage.setItem('ysp_portal_token', state.portalToken);
                sessionStorage.setItem('ysp_permissions', JSON.stringify(state.permissions));
                enterPortal();
            } else {
                $('ysp-login-error').textContent = resp.data.message || 'Incorrect PIN. Try again.';
                state.pin = '';
                updatePinDisplay();
            }
        }).fail(function () {
            $('ysp-login-error').textContent = 'Network error. Please try again.';
            state.pin = '';
            updatePinDisplay();
        }).always(function () {
            state.loginPending = false;
        });
    }

    /* Portal */
    function enterPortal() {
        $('ysp-staff-name').textContent = state.staffName;
        $('ysp-day-tabs').style.display = 'none';
        buildWeekDays();
        renderDayTabs();
        showScreen('ysp-schedule-screen');
        state.currentView = 'today';
        updateCalendarControls();
        loadSchedule();
        startPolling();
        registerSW();
        startIdleWatch();
    }

    /* Idle auto-lock - after N minutes without any tap/click/key, drop back
     * to the PIN screen so an unattended device doesn't stay signed in.
     * Session data is kept in memory; entering the correct PIN again just
     * re-verifies and resumes, it doesn't need to reload everything. */
    let idleTimer = null;
    let idleWatchStarted = false;

    function resetIdleTimer() {
        if (!state.staffId) return;
        clearTimeout(idleTimer);
        const minutes = parseInt(yspData.idleLockMinutes, 10);
        if (!minutes || minutes <= 0) return; // 0 = disabled by the business owner
        idleTimer = setTimeout(lockPortal, minutes * 60 * 1000);
    }

    function lockPortal() {
        if (!state.staffId) return;
        $('ysp-login-error').textContent = '';
        updatePinDisplay();
        showScreen('ysp-login-screen');
        toast('Locked after inactivity - enter your PIN to continue');
    }

    function startIdleWatch() {
        resetIdleTimer();
        if (idleWatchStarted) return;
        idleWatchStarted = true;
        ['click', 'touchstart', 'keydown', 'scroll'].forEach(evt => {
            document.addEventListener(evt, resetIdleTimer, { passive: true });
        });
    }

    function logout() {
        stopPolling();
        clearTimeout(idleTimer);
        state.staffId = null;
        state.staffName = null;
        state.staffRole = 'staff';
        state.portalToken = '';
        state.pin = '';
        state.schedule = {};
        state.clients = [];
        state.staffRoster = [];
        state.staffFilterInitialized = false;
        state.tab = 0;
        state.weekOffset = 0;
        state.selectedStaffIds = null;
        state.currentView = 'today';
        state.permissions = { view_schedule: true, view_all_schedules: false, update_status: false, create_booking: false, edit_booking: false, cancel_booking: false, view_clients: false, manage_clients: false, send_messages: false, checkout: false, request_time_off: false, approve_time_off: false };
        sessionStorage.removeItem('ysp_staff_id');
        sessionStorage.removeItem('ysp_staff_name');
        sessionStorage.removeItem('ysp_staff_role');
        sessionStorage.removeItem('ysp_portal_token');
        sessionStorage.removeItem('ysp_permissions');
        updatePinDisplay();
        $('ysp-login-error').textContent = '';
        showScreen('ysp-login-screen');
    }

    function setBottomNavActive(view) {
        document.querySelectorAll('.ysp-bottom-nav-btn').forEach(btn => {
            btn.classList.toggle('active', btn.dataset.view === view);
        });
    }

    function switchPortalView(view) {
        if (view === 'today') {
            state.currentView = 'today';
            setBottomNavActive('today');
            $('ysp-day-tabs').style.display = 'none';
            $('ysp-staff-tabs').style.display = 'none';
            updateCalendarControls();
            $('ysp-topbar-date').textContent = 'Today';
            renderToday();
            return;
        }
        if (view === 'calendar') {
            state.currentView = 'calendar';
            setBottomNavActive('calendar');
            $('ysp-day-tabs').style.display = '';
            renderStaffTabs();
            updateCalendarControls();
            renderDayTabs();
            renderSchedule();
            if (state.calendarMode === 'month') {
                $('ysp-topbar-date').textContent = monthGridInfo(state.monthAnchor).label;
            } else {
                const iso = state.weekDays[state.tab];
                $('ysp-topbar-date').textContent = formatDate(iso);
            }
            return;
        }
        if (view === 'clients') {
            if (!state.permissions.view_clients) {
                toast('Ask the owner to enable client access.');
                return;
            }
            state.currentView = 'clients';
            setBottomNavActive('clients');
            $('ysp-day-tabs').style.display = 'none';
            $('ysp-staff-tabs').style.display = 'none';
            updateCalendarControls();
            $('ysp-topbar-date').textContent = 'Customers';
            renderClientsView();
            return;
        }
        if (view === 'messages') {
            if (!state.permissions.send_messages) {
                toast('Ask the owner to enable messages.');
                return;
            }
            state.currentView = 'messages';
            setBottomNavActive('messages');
            $('ysp-day-tabs').style.display = 'none';
            $('ysp-staff-tabs').style.display = 'none';
            updateCalendarControls();
            $('ysp-topbar-date').textContent = 'Messages';
            renderMessagesView();
            return;
        }
        if (view === 'checkout') {
            if (!state.permissions.checkout) {
                toast('Ask the owner to enable checkout.');
                return;
            }
            state.currentView = 'checkout';
            setBottomNavActive('checkout');
            $('ysp-day-tabs').style.display = 'none';
            $('ysp-staff-tabs').style.display = 'none';
            updateCalendarControls();
            $('ysp-topbar-date').textContent = 'Checkout';
            renderCheckoutView();
        }
    }

    /* Day tabs */
    function renderDayTabs() {
        const container = $('ysp-day-tabs');
        if (state.calendarMode === 'month') {
            container.style.display = 'none';
            container.innerHTML = '';
            return;
        }
        if (state.currentView === 'calendar') container.style.display = '';
        container.innerHTML = '';
        const prev = document.createElement('button');
        prev.className = 'ysp-week-nav';
        prev.type = 'button';
        prev.textContent = 'Prev';
        prev.addEventListener('click', () => switchWeek(state.weekOffset - 1));
        container.appendChild(prev);

        state.weekDays.forEach((iso, i) => {
            const info = formatDayTab(iso);
            const count = (state.schedule[iso] || []).filter(b => isStaffVisible(b.staff_id)).length;
            const btn = document.createElement('button');
            btn.className = 'ysp-day-tab' + (i === state.tab ? ' active' : '');
            btn.innerHTML = `
                <span class="ysp-day-tab-day">${info.day}</span>
                <span class="ysp-day-tab-num">${info.num}</span>
                <span class="ysp-day-tab-count">${count}</span>
            `;
            btn.addEventListener('click', () => switchTab(i));
            container.appendChild(btn);
        });

        const todayBtn = document.createElement('button');
        todayBtn.className = 'ysp-week-nav' + (state.weekOffset === 0 ? ' active' : '');
        todayBtn.type = 'button';
        todayBtn.textContent = 'Today';
        todayBtn.addEventListener('click', () => switchWeek(0));
        container.appendChild(todayBtn);

        const next = document.createElement('button');
        next.className = 'ysp-week-nav';
        next.type = 'button';
        next.textContent = 'Next';
        next.addEventListener('click', () => switchWeek(state.weekOffset + 1));
        container.appendChild(next);
    }

    function switchWeek(offset) {
        state.weekOffset = offset;
        state.tab = 0;
        buildWeekDays();
        renderDayTabs();
        loadSchedule(false);
    }

    function viewTomorrow() {
        if (state.tab < state.weekDays.length - 1) {
            switchTab(state.tab + 1);
            return;
        }
        switchWeek(state.weekOffset + 1);
    }

    function switchTab(i) {
        state.tab = i;
        renderDayTabs();
        renderSchedule();
        const iso = state.weekDays[i];
        $('ysp-topbar-date').textContent = formatDate(iso);
    }

    /* Load schedule */
    function loadSchedule(silent = false) {
        if (!silent) {
            $('ysp-schedule-list').innerHTML = '<div class="ysp-loading-wrap"><div class="ysp-spinner"></div></div>';
            $('ysp-topbar-date').textContent = 'Loading...';
        }

        const isMonth = state.calendarMode === 'month';
        const monthInfo = isMonth ? monthGridInfo(state.monthAnchor) : null;

        jQuery.post(yspData.ajaxUrl, {
            action:   'yatoon_staff_get_schedule',
            nonce:    yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            date_from: isMonth ? monthInfo.monthStart : state.weekDays[0],
            date_to: isMonth ? monthInfo.monthEnd : state.weekDays[state.weekDays.length - 1],
        }, function (resp) {
            if (resp.success) {
                const newSchedule = resp.data.schedule || {};
                state.permissions = Object.assign({}, state.permissions, resp.data.permissions || {});
                state.staffRole = resp.data.role || state.staffRole;
                state.staffRoster = resp.data.staff || state.staffRoster;
                if (state.permissions.view_all_schedules && !state.staffFilterInitialized) {
                    state.selectedStaffIds = [String(state.staffId)];
                    state.staffFilterInitialized = true;
                }
                updatePermissionUI();
                sessionStorage.setItem('ysp_permissions', JSON.stringify(state.permissions));
                // Only re-render if data changed (avoid flicker on silent refresh)
                const changed = JSON.stringify(newSchedule) !== JSON.stringify(state.schedule);
                state.schedule = newSchedule;
                renderStaffTabs();
                if (state.currentView === 'calendar' && (!silent || changed)) {
                    renderDayTabs();
                    renderSchedule();
                }
                if (state.currentView === 'today' && (!silent || changed)) {
                    renderToday();
                    $('ysp-topbar-date').textContent = 'Today';
                }
                if (state.currentView === 'calendar' && !silent) {
                    if (state.calendarMode === 'month') {
                        $('ysp-topbar-date').textContent = monthGridInfo(state.monthAnchor).label;
                    } else {
                        const iso = state.weekDays[state.tab];
                        $('ysp-topbar-date').textContent = formatDate(iso);
                    }
                }
                if (silent && changed) {
                    toast('Schedule updated', 1500);
                }
                // Show last sync time
                const now = new Date();
                const syncEl = document.getElementById('ysp-last-sync');
                if (syncEl) {
                    syncEl.textContent = 'Synced ' + now.toLocaleTimeString('en-US', {hour:'numeric', minute:'2-digit'});
                }
            } else {
                if (!silent) {
                    $('ysp-schedule-list').innerHTML = '<div class="ysp-empty"><div class="ysp-empty-icon">Warning:</div><div class="ysp-empty-text">Failed to load schedule.</div></div>';
                }
            }
        }).fail(function () {
            if (!silent) {
                $('ysp-schedule-list').innerHTML = '<div class="ysp-empty"><div class="ysp-empty-icon">Offline</div><div class="ysp-empty-text">Network error. Pull down to refresh.</div></div>';
            }
        });
    }

    /* Auto-poll every 60 seconds */
    function startPolling() {
        stopPolling();
        state.pollTimer = setInterval(function () {
            if (state.staffId) loadSchedule(true); // silent = no spinner
        }, POLL_INTERVAL);
    }

    function stopPolling() {
        if (state.pollTimer) {
            clearInterval(state.pollTimer);
            state.pollTimer = null;
        }
    }

    /* Render schedule */
    function statusBadgeHTML(status) {
        const map = {
            confirmed: 'confirmed', pending: 'pending', arrived: 'arrived',
            no_show: 'no_show', cancelled: 'cancelled', completed: 'completed',
        };
        const cls = map[status] || 'pending';
        const label = status.replace('_', ' ');
        return `<span class="ysp-status-badge ${cls}">${label}</span>`;
    }

    function renderSchedule() {
        if (state.calendarMode === 'month') {
            renderMonthView();
            return;
        }
        const iso = state.weekDays[state.tab];
        const bookings = (state.schedule[iso] || []).sort((a, b) =>
            new Date(a.start_time) - new Date(b.start_time)
        );
        if (state.calendarMode === 'list') {
            renderScheduleList();
            return;
        }
        if (state.calendarMode === 'week') {
            renderWeekList();
            return;
        }
        renderTimeline(bookings, $('ysp-schedule-list'), iso);
    }

    function weekBookings() {
        return state.weekDays.flatMap(iso => (state.schedule[iso] || []).map(b => Object.assign({ _date: iso }, b)))
            .sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
    }

    function isStaffVisible(staffId) {
        if (!state.selectedStaffIds || !state.selectedStaffIds.length) {
            if (state.permissions.view_all_schedules && window.matchMedia && window.matchMedia('(max-width: 640px)').matches) {
                return String(staffId || state.staffId || '0') === String(state.staffId || '0');
            }
            return true;
        }
        return state.selectedStaffIds.map(String).includes(String(staffId || state.staffId || '0'));
    }

    function uniqueStaffForTimeline(bookings) {
        const map = new Map();
        bookings.forEach(b => {
            const id = String(b.staff_id || state.staffId || '0');
            if (!map.has(id)) map.set(id, { name: b.staff_name || state.staffName || 'Staff', color: b.staff_color || '' });
        });
        if (!map.size) map.set(String(state.staffId || '0'), { name: state.staffName || 'Staff', color: '' });
        return Array.from(map, ([id, info]) => ({ id, name: info.name, color: info.color }));
    }

    function validStaffColor(color) {
        return /^#[0-9a-f]{6}$/i.test(String(color || '')) ? color : '';
    }

    function renderStaffTabs() {
        const wrap = $('ysp-staff-tabs');
        if (!wrap) return;
        if (!state.permissions.view_all_schedules || state.currentView !== 'calendar' || state.staffRoster.length < 2) {
            wrap.style.display = 'none';
            wrap.innerHTML = '';
            return;
        }
        const selected = state.selectedStaffIds && state.selectedStaffIds.length ? String(state.selectedStaffIds[0]) : 'all';
        wrap.innerHTML = `
            <button type="button" class="ysp-staff-tab ysp-staff-tab-all ${selected === 'all' ? 'active' : ''}" data-staff-tab="all">All staff</button>
            ${state.staffRoster.map(staff => `
                <button type="button" class="ysp-staff-tab ${selected === String(staff.id) ? 'active' : ''}" data-staff-tab="${escapeHTML(staff.id)}">
                    <i style="background:${validStaffColor(staff.color) || '#111827'}"></i>${escapeHTML(staff.name)}
                </button>
            `).join('')}
        `;
        wrap.style.display = 'flex';
        wrap.querySelectorAll('[data-staff-tab]').forEach(btn => {
            btn.addEventListener('click', () => {
                state.selectedStaffIds = btn.dataset.staffTab === 'all' ? null : [btn.dataset.staffTab];
                renderStaffTabs();
                renderDayTabs();
                renderSchedule();
            });
        });
    }

    function todayBookings() {
        const now = new Date();
        const iso = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0');
        return (state.schedule[iso] || []).filter(b => isStaffVisible(b.staff_id)).sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
    }

    function renderToday() {
        const list = $('ysp-schedule-list');
        const bookings = groupBookingsForDisplay(todayBookings());
        const now = Date.now();
        const activeStatuses = ['arrived', 'completed', 'no_show', 'cancelled'];
        const next = bookings.find(b => new Date(b.start_time).getTime() >= now && !activeStatuses.includes(String(b.status || '').toLowerCase()));
        const unpaid = bookings.filter(b => !b.payment_status || b.payment_status === 'unpaid').length;
        const pending = bookings.filter(b => String(b.status || '').toLowerCase() === 'pending').length;
        const late = bookings.filter(b => new Date(b.start_time).getTime() < now && !activeStatuses.includes(String(b.status || '').toLowerCase())).length;
        list.innerHTML = `
            <div class="ysp-today-head">
                <span>${escapeHTML(state.staffRole.charAt(0).toUpperCase() + state.staffRole.slice(1))} workspace</span>
                <h2>Good ${new Date().getHours() < 12 ? 'morning' : (new Date().getHours() < 18 ? 'afternoon' : 'evening')}, ${escapeHTML(state.staffName || '')}</h2>
            </div>
            <div class="ysp-today-metrics">
                <div><span>Appointments</span><strong>${bookings.length}</strong></div>
                <div><span>Unpaid</span><strong>${unpaid}</strong></div>
                <div><span>Pending</span><strong>${pending}</strong></div>
                <div class="${late ? 'attention' : ''}"><span>Late</span><strong>${late}</strong></div>
            </div>
            <section class="ysp-today-next">
                <div class="ysp-today-section-title"><h3>Next client</h3><button type="button" data-open-calendar>Calendar</button></div>
                ${next ? `
                    <button type="button" class="ysp-next-client${next.design_image_url ? ' ysp-has-design' : ''}" data-today-booking="${next.id}">
                        <span class="ysp-next-time">${formatTime(next.start_time)}</span>
                        <span><strong>${escapeHTML(next.customer_name || 'Appointment')}</strong><em>${escapeHTML(next.service_name || 'Service')}${next.staff_name && state.permissions.view_all_schedules ? ' - ' + escapeHTML(next.staff_name) : ''}</em></span>
                        <b>${escapeHTML(paymentLabel(next.payment_status))}</b>
                    </button>
                ` : '<div class="ysp-today-open"><strong>No more appointments</strong><span>Your schedule is clear for the rest of today.</span></div>'}
            </section>
            <section class="ysp-today-agenda">
                <div class="ysp-today-section-title"><h3>Today</h3><span>${bookings.length} total</span></div>
                ${bookings.length ? bookings.map(b => `
                    <button type="button" class="ysp-today-row${b.design_image_url ? ' ysp-has-design' : ''}" data-today-booking="${b.id}">
                        <span>${formatTime(b.start_time)}</span>
                        <span><strong>${escapeHTML(b.customer_name || 'Appointment')}</strong><em>${escapeHTML(b.service_name || 'Service')}</em></span>
                        ${statusBadgeHTML(b.status)}
                    </button>
                `).join('') : '<div class="ysp-today-open"><strong>Open day</strong><span>No appointments are booked today.</span></div>'}
            </section>
        `;
        list.querySelectorAll('[data-today-booking]').forEach(btn => {
            btn.addEventListener('click', () => {
                const booking = bookings.find(b => String(b.id) === String(btn.dataset.todayBooking));
                if (booking) openDetail(booking);
            });
        });
        const calendarBtn = list.querySelector('[data-open-calendar]');
        if (calendarBtn) calendarBtn.addEventListener('click', () => switchPortalView('calendar'));
    }

    /**
     * When a customer books multiple services in one visit (e.g. Gel Manicure
     * + Regular Pedicure back to back), each service is its own row in the
     * database but they share a group_booking_id. Merge those into a single
     * display card - matching Square's calendar - instead of showing two
     * separate blocks with the same customer name.
     *
     * As a fallback for bookings that don't have a group_booking_id set
     * (older data, or edge cases where the link wasn't recorded), also merge
     * rows that are unmistakably the same visit: same customer, same staff,
     * and the exact same start time. This is intentionally strict (exact
     * time match, not just "close together") so it won't accidentally merge
     * two different customers' back-to-back appointments.
     */
    function groupBookingsForDisplay(bookings) {
        const buckets = {};
        const order = [];
        bookings.forEach(b => {
            const gid = b.group_booking_id || ('fallback:' + [
                (b.customer_name || '').trim().toLowerCase(),
                b.staff_id,
                new Date(b.start_time).getTime(),
            ].join('|'));
            if (!buckets[gid]) {
                buckets[gid] = [];
                order.push({ gid });
            }
            buckets[gid].push(b);
        });
        const result = order.map(entry => {
            const group = buckets[entry.gid];
            if (group.length === 1) return group[0];
            const sorted = group.slice().sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
            const merged = Object.assign({}, sorted[0]);
            merged._isGroup = true;
            merged._groupBookings = sorted;
            merged.start_time = Math.min(...sorted.map(x => new Date(x.start_time).getTime()));
            merged.end_time = Math.max(...sorted.map(x => new Date(x.end_time).getTime()));
            merged.total_price = sorted.reduce((sum, x) => sum + (parseFloat(x.total_price) || 0), 0);
            merged.service_name = sorted.map(x => x.service_name || 'Service').join(', ');
            merged.design_image_url = sorted.map(x => x.design_image_url).find(Boolean) || '';
            // If any sub-service still needs payment, surface that on the merged card.
            merged.payment_status = sorted.some(x => x.payment_status === 'unpaid') ? 'unpaid' : sorted[0].payment_status;
            return merged;
        });
        return result.sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
    }


    function renderTimeline(bookings, list, iso) {
        bookings = groupBookingsForDisplay(bookings.filter(b => isStaffVisible(b.staff_id)));
        const staffColsRaw = state.permissions.view_all_schedules && state.staffRoster.length ? state.staffRoster : [{ id: String(state.staffId), name: state.staffName || 'Staff' }];
        const staffCols = staffColsRaw.filter(s => isStaffVisible(s.id));
        const bookingStarts = bookings.map(b => new Date(b.start_time));
        const bookingEnds = bookings.map(b => new Date(b.end_time));
        const startHour = bookingStarts.length
            ? Math.max(6, Math.min(...bookingStarts.map(d => d.getHours())) - 1)
            : 9;
        const endHour = bookingEnds.length
            ? Math.min(22, Math.max(...bookingEnds.map(d => d.getHours() + (d.getMinutes() ? 1 : 0))) + 1)
            : 18;
        const compactTimeline = window.matchMedia && window.matchMedia('(max-width: 640px)').matches;
        const desktopTimeline = window.matchMedia && window.matchMedia('(min-width: 900px)').matches;
        const hourHeight = compactTimeline ? 72 : (desktopTimeline ? 80 : 76);
        const timeColWidth = compactTimeline ? 42 : 56;
        const staffColMin = compactTimeline && staffCols.length === 1 ? 260 : (compactTimeline ? 150 : (desktopTimeline ? 220 : 180));
        const totalHeight = (endHour - startHour) * hourHeight;
        const nextBooking = bookings.find(b => new Date(b.start_time) >= new Date());
        const unpaidCount = bookings.filter(b => !b.payment_status || b.payment_status === 'unpaid').length;

        if (!bookings.length && !state.permissions.create_booking) {
            renderScheduleList();
            return;
        }

        list.innerHTML = `
            <div class="ysp-date-heading">${formatDate(iso)}</div>
            ${!bookings.length ? `
            <div class="ysp-empty ysp-empty-actionable">
                <div class="ysp-empty-icon">Open</div>
                <div class="ysp-empty-text">No appointments are booked for this day.</div>
                <div class="ysp-empty-actions">
                    ${state.permissions.create_booking ? '<button type="button" class="ysp-small-primary" data-empty-add="1">Add appointment</button>' : ''}
                    <button type="button" class="ysp-mini-ghost" data-empty-tomorrow="1">View tomorrow</button>
                </div>
            </div>` : ''}
            <div class="ysp-insight-strip">
                <div class="ysp-insight-card">
                    <span>Appointments</span>
                    <strong>${bookings.length}</strong>
                </div>
                <div class="ysp-insight-card">
                    <span>Next up</span>
                    <strong>${nextBooking ? formatTime(nextBooking.start_time) : 'Open'}</strong>
                </div>
                <div class="ysp-insight-card">
                    <span>Checkout</span>
                    <strong>${unpaidCount}</strong>
                </div>
            </div>
            <div class="ysp-calendar-shell">
                <div class="ysp-calendar-head" style="grid-template-columns:${timeColWidth}px repeat(${staffCols.length}, minmax(${staffColMin}px, 1fr));">
                    <div class="ysp-time-head"></div>
                    ${staffCols.map(s => `<div class="ysp-staff-head">${escapeHTML(s.name)}</div>`).join('')}
                </div>
                <div class="ysp-calendar-grid" style="grid-template-columns:${timeColWidth}px repeat(${staffCols.length}, minmax(${staffColMin}px, 1fr));height:${totalHeight}px;">
                    <div class="ysp-time-rail">
                        ${Array.from({ length: endHour - startHour + 1 }, (_, i) => `<div class="ysp-hour-label" style="top:${i * hourHeight}px">${formatHour(startHour + i)}</div>`).join('')}
                    </div>
                    ${staffCols.map(s => `
                        <div class="ysp-staff-col" data-staff-id="${s.id}">
                            ${Array.from({ length: endHour - startHour }, (_, i) => `<button type="button" class="ysp-time-slot" data-hour="${startHour + i}" style="top:${i * hourHeight}px;height:${hourHeight}px;"></button>`).join('')}
                        </div>
                    `).join('')}
                </div>
            </div>
        `;

        const emptyAdd = list.querySelector('[data-empty-add]');
        if (emptyAdd) emptyAdd.addEventListener('click', () => openBookingEditor(null));
        const emptyTomorrow = list.querySelector('[data-empty-tomorrow]');
        if (emptyTomorrow) emptyTomorrow.addEventListener('click', viewTomorrow);

        document.querySelectorAll('.ysp-time-slot').forEach(slot => {
            slot.addEventListener('click', function() {
                if (!state.permissions.create_booking) return;
                const staffId = this.closest('.ysp-staff-col').dataset.staffId;
                openBookingEditor(null, {
                    staff_id: staffId,
                    start_time: isoForSelectedDay(parseInt(this.dataset.hour, 10), 0).getTime(),
                });
            });
        });

        function layoutTimelineBookings(items) {
            const sorted = items.slice().sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
            const lanes = [];
            return sorted.map(item => {
                const start = new Date(item.start_time).getTime();
                const end = new Date(item.end_time).getTime();
                let lane = lanes.findIndex(laneEnd => laneEnd <= start);
                if (lane < 0) {
                    lane = lanes.length;
                    lanes.push(end);
                } else {
                    lanes[lane] = end;
                }
                return { booking: item, lane, laneCount: 1 };
            }).map((entry, _, arranged) => {
                const start = new Date(entry.booking.start_time).getTime();
                const end = new Date(entry.booking.end_time).getTime();
                const overlapping = arranged.filter(other => {
                    const otherStart = new Date(other.booking.start_time).getTime();
                    const otherEnd = new Date(other.booking.end_time).getTime();
                    return otherStart < end && otherEnd > start;
                });
                entry.laneCount = Math.max(1, ...overlapping.map(other => other.lane + 1));
                return entry;
            });
        }

        staffCols.forEach((staff, index) => {
            const col = Array.from(list.querySelectorAll('.ysp-staff-col')).find(el => String(el.dataset.staffId) === String(staff.id));
            if (!col) return;
            layoutTimelineBookings(bookings.filter(b => String(b.staff_id || state.staffId) === String(staff.id))).forEach(entry => {
                const b = entry.booking;
                const start = new Date(b.start_time);
                const end = new Date(b.end_time);
                const rawTop = Math.max(0, ((start.getHours() + start.getMinutes() / 60) - startHour) * hourHeight);
                const durationMinutes = Math.max(0, (end - start) / 60000);
                const visualMinutes = Math.min(durationMinutes || 45, compactTimeline ? 75 : 90);
                const height = Math.max(compactTimeline ? 34 : 54, (visualMinutes / 60) * hourHeight);
                const top = Math.min(rawTop, Math.max(0, totalHeight - Math.min(height, totalHeight)));
                const laneGap = 4;
                const laneWidth = 100 / entry.laneCount;
                const card = document.createElement('button');
                card.type = 'button';
                const staffColor = validStaffColor(b.staff_color || staff.color);
                card.className = 'ysp-timeline-booking' + (staffColor ? '' : ' ysp-timeline-color-' + (index % 5)) + (b.design_image_url ? ' ysp-has-design' : '');
                if (staffColor) card.style.background = staffColor;
                card.style.setProperty('top', top + 'px', 'important');
                card.style.setProperty('height', Math.min(height, totalHeight - top) + 'px', 'important');
                card.style.setProperty('left', `calc(${entry.lane * laneWidth}% + ${entry.lane ? laneGap / 2 : 0}px)`, 'important');
                card.style.setProperty('right', 'auto', 'important');
                card.style.setProperty('width', `calc(${laneWidth}% - ${entry.laneCount > 1 ? laneGap : 0}px)`, 'important');
                if (compactTimeline && durationMinutes < 45) {
                    card.classList.add('is-short');
                }
                if (durationMinutes < 75) {
                    card.classList.add('is-compact');
                }
                card.innerHTML = `
                    <span class="ysp-timeline-time">${formatTime(b.start_time)}</span>
                    <strong>${escapeHTML(b.customer_name || 'Appointment')}</strong>
                    <span>${escapeHTML(b.service_name || 'Service')}</span>
                    <span class="ysp-timeline-meta">${escapeHTML(paymentLabel(b.payment_status))}</span>
                `;
                card.addEventListener('click', function(e) {
                    e.stopPropagation();
                    openDetail(b);
                });
                col.appendChild(card);
            });
        });
    }

    function renderScheduleList() {
        const iso = state.weekDays[state.tab];
        const bookings = groupBookingsForDisplay((state.schedule[iso] || []).filter(b => isStaffVisible(b.staff_id)).sort((a, b) =>
            new Date(a.start_time) - new Date(b.start_time)
        ));
        const list = $('ysp-schedule-list');

        if (!bookings.length) {
            list.innerHTML = `<div class="ysp-empty">
                <div class="ysp-empty-icon">Open</div>
                <div class="ysp-empty-text">No appointments are booked for this day.</div>
                <div class="ysp-empty-actions">
                    ${state.permissions.create_booking ? '<button type="button" class="ysp-small-primary" data-empty-add="1">Add appointment</button>' : ''}
                    <button type="button" class="ysp-mini-ghost" data-empty-tomorrow="1">View tomorrow</button>
                </div>
            </div>`;
            const emptyAdd = list.querySelector('[data-empty-add]');
            if (emptyAdd) emptyAdd.addEventListener('click', () => openBookingEditor(null));
            const emptyTomorrow = list.querySelector('[data-empty-tomorrow]');
            if (emptyTomorrow) emptyTomorrow.addEventListener('click', viewTomorrow);
            return;
        }

        list.innerHTML = `<div class="ysp-date-heading">${formatDate(iso)}</div>`;
        bookings.forEach(b => {
            const card = document.createElement('div');
            card.className = 'ysp-card' + (b.design_image_url ? ' ysp-has-design' : '');
            card.dataset.bookingId = b.id;
            const duration = Math.round((new Date(b.end_time) - new Date(b.start_time)) / 60000);
            card.innerHTML = `
                <div class="ysp-card-row1">
                    <div>
                        <div class="ysp-card-time">${formatTime(b.start_time)}</div>
                        <div class="ysp-card-duration">${duration} min</div>
                    </div>
                    ${statusBadgeHTML(b.status)}
                </div>
                <div class="ysp-card-customer">${escapeHTML(b.customer_name)}</div>
                <div class="ysp-card-service">${escapeHTML(b.service_name || 'Service')}${state.permissions.view_all_schedules && b.staff_name ? ' - ' + escapeHTML(b.staff_name) : ''}</div>
                <div class="ysp-card-price">$${Math.abs(parseFloat(b.total_price || 0)).toFixed(2)}</div>
                ${b.notes ? `<div class="ysp-card-notes">${escapeHTML(b.notes)}</div>` : ''}
            `;
            card.addEventListener('click', () => openDetail(b));
            list.appendChild(card);
        });
    }

    function renderWeekList() {
        const list = $('ysp-schedule-list');
        const bookings = weekBookings().filter(b => isStaffVisible(b.staff_id));
        if (!bookings.length) {
            list.innerHTML = `<div class="ysp-empty"><div class="ysp-empty-icon">Open</div><div class="ysp-empty-text">No appointments match this calendar view.</div></div>`;
            return;
        }
        const grouped = new Map();
        bookings.forEach(b => {
            const key = b._date || state.weekDays[state.tab];
            if (!grouped.has(key)) grouped.set(key, []);
            grouped.get(key).push(b);
        });
        grouped.forEach((dayBookings, key) => grouped.set(key, groupBookingsForDisplay(dayBookings)));
        list.innerHTML = Array.from(grouped, ([iso, dayBookings]) => `
            <div class="ysp-week-group">
                <div class="ysp-date-heading">${formatDate(iso)}</div>
                <div class="ysp-client-list">
                    ${dayBookings.map(b => `
                        <button type="button" class="ysp-client-card${b.design_image_url ? ' ysp-has-design' : ''}" data-week-booking="${b.id}">
                            <span class="ysp-client-avatar">${formatTime(b.start_time).replace(':00', '')}</span>
                            <span class="ysp-client-main">
                                <strong>${escapeHTML(b.customer_name || 'Appointment')}</strong>
                                <span>${escapeHTML(b.service_name || 'Service')}${b.staff_name ? ' - ' + escapeHTML(b.staff_name) : ''}</span>
                            </span>
                            <span class="ysp-client-meta">${escapeHTML(paymentLabel(b.payment_status))}</span>
                        </button>
                    `).join('')}
                </div>
            </div>
        `).join('');
        list.querySelectorAll('[data-week-booking]').forEach(btn => {
            btn.addEventListener('click', () => {
                const allGrouped = Array.from(grouped.values()).flat();
                const booking = allGrouped.find(b => String(b.id) === String(btn.dataset.weekBooking));
                openDetail(booking);
            });
        });
    }

    /* Simplified month calendar (for staff who just need to see when they have appointments) */
    function renderMonthView() {
        const list = $('ysp-schedule-list');
        const info = monthGridInfo(state.monthAnchor);
        const todayIso = (function () { const d = new Date(); d.setHours(0,0,0,0); return d.toISOString().slice(0, 10); })();
        if (!state.selectedDate || state.selectedDate < info.monthStart || state.selectedDate > info.monthEnd) {
            state.selectedDate = (todayIso >= info.monthStart && todayIso <= info.monthEnd) ? todayIso : info.monthStart;
        }
        const weekdayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

        const cellsHtml = info.cells.map(cell => {
            const dayBookings = cell.inMonth ? groupBookingsForDisplay((state.schedule[cell.iso] || []).filter(b => isStaffVisible(b.staff_id))) : [];
            const dayNum = parseInt(cell.iso.split('-')[2], 10);
            const classes = ['ysp-month-cell'];
            if (!cell.inMonth) classes.push('is-outside');
            if (cell.iso === todayIso) classes.push('is-today');
            if (cell.iso === state.selectedDate) classes.push('is-selected');
            if (dayBookings.some(b => b.design_image_url)) classes.push('has-design');
            return `
                <button type="button" class="${classes.join(' ')}" data-month-day="${cell.iso}">
                    <span class="ysp-month-daynum">${dayNum}</span>
                    ${dayBookings.length ? `<span class="ysp-month-dot-row">${dayBookings.slice(0, 3).map(() => '<span class="ysp-month-dot"></span>').join('')}${dayBookings.length > 3 ? `<span class="ysp-month-more">+${dayBookings.length - 3}</span>` : ''}</span>` : ''}
                </button>
            `;
        }).join('');

        const selectedBookings = groupBookingsForDisplay((state.schedule[state.selectedDate] || [])
            .filter(b => isStaffVisible(b.staff_id))
            .sort((a, b) => new Date(a.start_time) - new Date(b.start_time)));

        list.innerHTML = `
            <div class="ysp-month-header">
                <button type="button" class="ysp-week-nav" data-month-prev>Prev</button>
                <strong class="ysp-month-label">${info.label}</strong>
                <button type="button" class="ysp-week-nav" data-month-next>Next</button>
            </div>
            <div class="ysp-month-weekday-row">
                ${weekdayLabels.map(w => `<span>${w}</span>`).join('')}
            </div>
            <div class="ysp-month-grid">${cellsHtml}</div>
            <div class="ysp-month-agenda">
                <div class="ysp-date-heading">${formatDate(state.selectedDate)}</div>
                ${!selectedBookings.length ? `
                    <div class="ysp-empty">
                        <div class="ysp-empty-icon">Open</div>
                        <div class="ysp-empty-text">No appointments this day.</div>
                    </div>
                ` : selectedBookings.map(b => `
                    <button type="button" class="ysp-client-card${b.design_image_url ? ' ysp-has-design' : ''}" data-month-booking="${b.id}">
                        <span class="ysp-client-avatar">${formatTime(b.start_time).replace(':00', '')}</span>
                        <span class="ysp-client-main">
                            <strong>${escapeHTML(b.customer_name || 'Appointment')}</strong>
                            <span>${escapeHTML(b.service_name || 'Service')}${state.permissions.view_all_schedules && b.staff_name ? ' - ' + escapeHTML(b.staff_name) : ''}</span>
                        </span>
                        <span class="ysp-client-meta">${escapeHTML(paymentLabel(b.payment_status))}</span>
                    </button>
                `).join('')}
            </div>
        `;

        list.querySelector('[data-month-prev]').addEventListener('click', () => changeMonth(-1));
        list.querySelector('[data-month-next]').addEventListener('click', () => changeMonth(1));
        list.querySelectorAll('[data-month-day]').forEach(btn => {
            btn.addEventListener('click', () => {
                const iso = btn.dataset.monthDay;
                if (iso < info.monthStart) {
                    state.selectedDate = iso;
                    changeMonth(-1);
                } else if (iso > info.monthEnd) {
                    state.selectedDate = iso;
                    changeMonth(1);
                } else {
                    state.selectedDate = iso;
                    renderMonthView();
                }
            });
        });
        list.querySelectorAll('[data-month-booking]').forEach(btn => {
            btn.addEventListener('click', () => {
                const booking = selectedBookings.find(b => String(b.id) === String(btn.dataset.monthBooking));
                if (booking) openDetail(booking);
            });
        });
    }

    /* Clients and messages */
    function loadClients(term = '', done) {
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_get_clients',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            term,
        }, function(resp) {
            if (!resp.success) {
                toast(resp.data && resp.data.message ? resp.data.message : 'Could not load clients');
                if (done) done([]);
                return;
            }
            state.clients = resp.data.clients || [];
            state.permissions = Object.assign({}, state.permissions, resp.data.permissions || {});
            updatePermissionUI();
            if (done) done(state.clients, resp.data || {});
        }).fail(function() {
            toast('Network error');
            if (done) done([]);
        });
    }

    function clientById(id) {
        return state.clients.find(c => String(c.id) === String(id));
    }

    function clientFromBooking(booking) {
        const email = String(booking.customer_email || '').toLowerCase();
        const phone = String(booking.customer_phone || '').replace(/\D/g, '');
        const name = String(booking.customer_name || '').toLowerCase();
        const match = state.clients.find(c => {
            const cEmail = String(c.customer_email || '').toLowerCase();
            const cPhone = String(c.customer_phone || '').replace(/\D/g, '');
            const cName = String(c.customer_name || '').toLowerCase();
            return (email && cEmail === email) || (phone && cPhone === phone) || (name && cName === name);
        });
        return match || {
            id: booking.client_id || 0,
            booking_id: booking.id,
            customer_name: booking.customer_name || 'Appointment',
            customer_email: booking.customer_email || '',
            customer_phone: booking.customer_phone || '',
            allergies: booking.client_allergies || '',
            preferences: booking.client_preferences || '',
            notes: booking.client_notes || '',
            tags: booking.client_tags || '',
            booking_count: booking.client_booking_count || 0,
            last_booking: booking.client_last_booking || booking.start_time || '',
        };
    }

    function renderClientsView() {
        const list = $('ysp-schedule-list');
        list.innerHTML = `
            <div class="ysp-app-view-head">
                <div>
                    <div class="ysp-date-heading">Customers</div>
                    <p class="ysp-view-sub">Client profiles, notes, and contact shortcuts.</p>
                </div>
                ${state.permissions.manage_clients ? '<button type="button" class="ysp-small-primary" id="ysp-add-client-btn">Add</button>' : ''}
            </div>
            <input id="ysp-client-search" class="ysp-editor-input ysp-search-input" type="search" placeholder="Search name, phone, or email">
            <div class="ysp-insight-strip ysp-client-insights" id="ysp-client-insights">
                <div class="ysp-insight-card"><span>Total clients</span><strong>-</strong></div>
                <div class="ysp-insight-card"><span>With phone</span><strong>-</strong></div>
                <div class="ysp-insight-card"><span>Repeat visits</span><strong>-</strong></div>
            </div>
            <div class="ysp-client-list" id="ysp-client-list"><div class="ysp-loading-wrap"><div class="ysp-spinner"></div></div></div>
        `;
        if ($('ysp-add-client-btn')) $('ysp-add-client-btn').addEventListener('click', () => openClientEditor(null));
        $('ysp-client-search').addEventListener('input', debounce(function() {
            loadClients($('ysp-client-search').value.trim(), renderClientList);
        }, 250));
        loadClients('', renderClientList);
    }

    function renderClientList(clients) {
        const wrap = $('ysp-client-list');
        if (!wrap) return;
        const insights = $('ysp-client-insights');
        if (insights) {
            const withPhone = clients.filter(c => c.customer_phone).length;
            const repeat = clients.filter(c => parseInt(c.booking_count || 0, 10) > 1).length;
            insights.innerHTML = `
                <div class="ysp-insight-card"><span>Total clients</span><strong>${clients.length}</strong></div>
                <div class="ysp-insight-card"><span>With phone</span><strong>${withPhone}</strong></div>
                <div class="ysp-insight-card"><span>Repeat visits</span><strong>${repeat}</strong></div>
            `;
        }
        if (!clients.length) {
            wrap.innerHTML = '<div class="ysp-empty"><div class="ysp-empty-icon">No clients</div><div class="ysp-empty-text">New clients will appear here after booking.</div></div>';
            return;
        }
        wrap.innerHTML = clients.map(c => `
            <button type="button" class="ysp-client-card" data-client-id="${c.id}">
                <span class="ysp-client-avatar">${escapeHTML((c.customer_name || 'C').charAt(0).toUpperCase())}</span>
                <span class="ysp-client-main">
                    <strong>${escapeHTML(c.customer_name || 'Unnamed client')}</strong>
                    <span>${escapeHTML(c.customer_phone || c.customer_email || 'No contact saved')}</span>
                </span>
                <span class="ysp-client-meta">${parseInt(c.booking_count || 0, 10)} visits</span>
            </button>
        `).join('');
        wrap.querySelectorAll('[data-client-id]').forEach(btn => {
            btn.addEventListener('click', () => openClientDetail(clientById(btn.dataset.clientId)));
        });
    }

    function renderClientFacts(c) {
        return `
            <div class="ysp-client-facts">
                <div><span>Phone</span><strong>${escapeHTML(c.customer_phone || '-')}</strong></div>
                <div><span>Email</span><strong>${escapeHTML(c.customer_email || '-')}</strong></div>
                <button type="button" class="ysp-client-fact-btn" data-load-client-history="1"><span>Visits</span><strong>${parseInt(c.booking_count || 0, 10)}</strong></button>
                <div><span>Last visit</span><strong>${escapeHTML(formatDateValue(c.last_booking))}</strong></div>
            </div>
        `;
    }

    function renderClientHistoryRows(history) {
        if (!history || !history.length) {
            return '<div class="ysp-empty ysp-client-history-empty"><div class="ysp-empty-text">No visit history yet.</div></div>';
        }
        return `
            <div class="ysp-client-history-list">
                ${history.map(b => {
                    const bookedOn = b.created_at ? `Booked ${formatDateValue(b.created_at)} ${formatTime(b.created_at)}` : '';
                    return `
                        <div class="ysp-client-history-row">
                            <span>
                                <strong>${escapeHTML(formatDateValue(b.start_time))} ${escapeHTML(formatTime(b.start_time))}</strong>
                                <em>${escapeHTML(b.service_name || 'Service')}${b.staff_name ? ' - ' + escapeHTML(b.staff_name) : ''}</em>
                                ${b.notes ? `<em>${escapeHTML(b.notes)}</em>` : ''}
                                ${bookedOn ? `<em>${escapeHTML(bookedOn)}</em>` : ''}
                            </span>
                            <span>
                                <strong>$${Math.abs(parseFloat(b.total_price || 0)).toFixed(2)}</strong>
                                <em>${escapeHTML(b.status || '-')} / ${escapeHTML(paymentLabel(b.payment_status))}</em>
                            </span>
                        </div>
                    `;
                }).join('')}
            </div>
        `;
    }

    function loadClientHistory(client, target) {
        if (!client || !client.id || !target) return;
        target.innerHTML = '<div class="ysp-loading-wrap"><div class="ysp-spinner"></div></div>';
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_get_client_history',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            client_id: client.id,
        }, function(resp) {
            if (resp.success) {
                target.innerHTML = renderClientHistoryRows(resp.data.history || []);
            } else {
                target.innerHTML = '<div class="ysp-empty"><div class="ysp-empty-text">Could not load visits.</div></div>';
            }
        }).fail(function() {
            target.innerHTML = '<div class="ysp-empty"><div class="ysp-empty-text">Network error loading visits.</div></div>';
        });
    }

    function openClientDetail(client) {
        if (!client) return;
        if (!state.permissions.view_clients) {
            showClientAccessDenied();
            return;
        }
        state.activeClient = client;
        $('ysp-sheet-body').innerHTML = `
            <div class="ysp-detail-top ysp-client-detail-top">
                <button type="button" class="ysp-detail-close" data-ysp-close="detail" aria-label="Close">&times;</button>
            </div>
            <div class="ysp-sheet-time">${escapeHTML(client.customer_name || 'Unnamed client')}</div>
            <div class="ysp-sheet-service">Customer profile</div>
            ${renderClientFacts(client)}
            ${client.preferences ? `<div class="ysp-sheet-notes-box"><strong>Preferences</strong><br>${escapeHTML(client.preferences)}</div>` : ''}
            ${client.allergies ? `<div class="ysp-sheet-notes-box"><strong>Allergies</strong><br>${escapeHTML(client.allergies)}</div>` : ''}
            <div class="ysp-sheet-notes-box"><strong>Staff / Admin Notes</strong><br>${client.notes ? escapeHTML(client.notes) : '<span class="ysp-muted">No notes yet. Tap Edit to add preferences, cautions, or service reminders.</span>'}</div>
            <section class="ysp-square-section ysp-client-history-section">
                <h3>Visit history</h3>
                <div id="ysp-client-history-wrap"><button type="button" class="ysp-square-row-action" data-load-client-history="1"><span>View past appointments</span><strong>Visits</strong></button></div>
            </section>
            <div class="ysp-sheet-actions">
                ${client.customer_phone ? `<button class="ysp-action-btn call" onclick="window.location.href='tel:${escapeHTML(client.customer_phone)}'">Call</button>` : ''}
                ${state.permissions.send_messages ? '<button class="ysp-action-btn call" data-message-client="1">Message</button>' : ''}
                ${state.permissions.manage_clients && client.id ? '<button class="ysp-action-btn call" data-edit-client="1">Edit</button>' : ''}
                ${state.permissions.manage_clients && client.id ? '<button class="ysp-action-btn no-show" data-delete-client="1">Delete</button>' : ''}
            </div>
        `;
        const editBtn = $('ysp-sheet-body').querySelector('[data-edit-client]');
        if (editBtn) editBtn.addEventListener('click', () => openClientEditor(client));
        const deleteBtn = $('ysp-sheet-body').querySelector('[data-delete-client]');
        if (deleteBtn) deleteBtn.addEventListener('click', () => deleteClient(client.id));
        const messageBtn = $('ysp-sheet-body').querySelector('[data-message-client]');
        if (messageBtn) messageBtn.addEventListener('click', () => openMessageComposer(client));
        $('ysp-sheet-body').querySelectorAll('[data-load-client-history]').forEach(btn => {
            btn.addEventListener('click', e => {
                e.preventDefault();
                e.stopPropagation();
                const wrap = $('ysp-client-history-wrap');
                loadClientHistory(client, wrap);
            });
        });
        $('ysp-detail-overlay').classList.remove('ysp-settings-overlay', 'ysp-checkout-overlay');
        $('ysp-detail-overlay').classList.add('open');
    }

    function openClientEditor(client) {
        if (!state.permissions.manage_clients) {
            toast('Ask the owner to enable client management.');
            return;
        }
        $('ysp-editor-body').innerHTML = `
            <div class="ysp-sheet-time">${client ? 'Edit client' : 'Add client'}</div>
            ${editorField('ysp-client-name', 'Name', 'text', client ? client.customer_name : '')}
            <div class="ysp-editor-grid">
                <div>${editorField('ysp-client-phone', 'Phone', 'tel', client ? client.customer_phone : '')}</div>
                <div>${editorField('ysp-client-email', 'Email', 'email', client ? client.customer_email : '')}</div>
            </div>
            <label class="ysp-editor-label" for="ysp-client-preferences">Preferences</label>
            <textarea id="ysp-client-preferences" class="ysp-editor-textarea">${escapeHTML(client ? client.preferences : '')}</textarea>
            <label class="ysp-editor-label" for="ysp-client-allergies">Allergies</label>
            <textarea id="ysp-client-allergies" class="ysp-editor-textarea">${escapeHTML(client ? client.allergies : '')}</textarea>
            ${editorField('ysp-client-tags', 'Tags', 'text', client ? client.tags : '')}
            <label class="ysp-editor-label" for="ysp-client-notes">Notes</label>
            <textarea id="ysp-client-notes" class="ysp-editor-textarea">${escapeHTML(client ? client.notes : '')}</textarea>
            <div class="ysp-sheet-actions">
                <button class="ysp-action-btn complete" id="ysp-save-client">${client ? 'Save client' : 'Add client'}</button>
                <button class="ysp-action-btn cancel" id="ysp-cancel-client-editor">Close</button>
            </div>
        `;
        $('ysp-editor-overlay').classList.add('open');
        $('ysp-cancel-client-editor').addEventListener('click', closeEditor);
        $('ysp-save-client').addEventListener('click', () => saveClient(client ? client.id : 0));
    }

    function saveClient(clientId) {
        const btn = $('ysp-save-client');
        btn.disabled = true;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_save_client',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            client_id: clientId || 0,
            customer_name: $('ysp-client-name').value.trim(),
            customer_phone: $('ysp-client-phone').value.trim(),
            customer_email: $('ysp-client-email').value.trim(),
            preferences: $('ysp-client-preferences').value,
            allergies: $('ysp-client-allergies').value,
            tags: $('ysp-client-tags').value,
            notes: $('ysp-client-notes').value,
        }, function(resp) {
            btn.disabled = false;
            if (resp.success) {
                toast(resp.data.message || 'Client saved');
                closeEditor();
                closeDetail();
                renderClientsView();
            } else {
                toast(resp.data && resp.data.message ? resp.data.message : 'Could not save client');
            }
        }).fail(function() {
            btn.disabled = false;
            toast('Network error');
        });
    }

    function deleteClient(clientId) {
        if (!window.confirm('Delete this client profile? Booking history will stay.')) return;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_delete_client',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            client_id: clientId,
        }, function(resp) {
            if (resp.success) {
                toast(resp.data.message || 'Client deleted');
                closeDetail();
                renderClientsView();
            } else {
                toast(resp.data && resp.data.message ? resp.data.message : 'Could not delete client');
            }
        });
    }

    function renderMessagesView() {
        const list = $('ysp-schedule-list');
        list.innerHTML = `
            <div class="ysp-app-view-head">
                <div>
                    <div class="ysp-date-heading">Messages</div>
                    <p class="ysp-view-sub">Send SMS through Twilio or open WhatsApp with a ready message.</p>
                </div>
            </div>
            <input id="ysp-message-search" class="ysp-editor-input ysp-search-input" type="search" placeholder="Search customer to message">
            <div class="ysp-client-list" id="ysp-message-client-list"><div class="ysp-loading-wrap"><div class="ysp-spinner"></div></div></div>
        `;
        $('ysp-message-search').addEventListener('input', debounce(function() {
            loadClients($('ysp-message-search').value.trim(), renderMessageClientList);
        }, 250));
        loadClients('', renderMessageClientList);
    }

    function renderMessageClientList(clients) {
        const wrap = $('ysp-message-client-list');
        if (!wrap) return;
        const withPhone = clients.filter(c => c.customer_phone);
        if (!withPhone.length) {
            wrap.innerHTML = '<div class="ysp-empty"><div class="ysp-empty-icon">No phone</div><div class="ysp-empty-text">Clients need a phone number for SMS or WhatsApp.</div></div>';
            return;
        }
        wrap.innerHTML = withPhone.map(c => `
            <button type="button" class="ysp-client-card" data-message-client-id="${c.id}">
                <span class="ysp-client-avatar">${escapeHTML((c.customer_name || 'C').charAt(0).toUpperCase())}</span>
                <span class="ysp-client-main">
                    <strong>${escapeHTML(c.customer_name || 'Unnamed client')}</strong>
                    <span>${escapeHTML(c.customer_phone || '')}</span>
                </span>
                <span class="ysp-client-meta">Message</span>
            </button>
        `).join('');
        wrap.querySelectorAll('[data-message-client-id]').forEach(btn => {
            btn.addEventListener('click', () => openMessageComposer(clientById(btn.dataset.messageClientId)));
        });
    }

    function activeDayBookings() {
        const iso = state.weekDays[state.tab];
        return (state.schedule[iso] || []).slice().sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
    }

    function paymentLabel(status) {
        const map = {
            unpaid: 'Unpaid',
            paid_square_pos: 'Paid',
            deposit_paid: 'Deposit paid',
            refunded: 'Refunded',
            waived: 'Waived',
            no_show_fee_charged: 'No-show fee charged',
            no_show_fee_waived: 'No-show fee waived',
        };
        return map[status || 'unpaid'] || String(status || 'unpaid').replace(/_/g, ' ');
    }

    function renderCheckoutView() {
        const bookings = groupBookingsForDisplay(activeDayBookings());
        const unpaid = bookings.filter(b => !b.payment_status || b.payment_status === 'unpaid');
        const list = $('ysp-schedule-list');
        list.innerHTML = `
            <div class="ysp-app-view-head">
                <div>
                    <div class="ysp-date-heading">Checkout</div>
                    <p class="ysp-view-sub">Collect in person, mark paid, or send a payment request.</p>
                </div>
                <span class="ysp-checkout-total">$${unpaid.reduce((sum, b) => sum + Math.abs(parseFloat(b.total_price || 0)), 0).toFixed(2)}</span>
            </div>
            <div class="ysp-client-list" id="ysp-checkout-list"></div>
        `;
        const wrap = $('ysp-checkout-list');
        if (!bookings.length) {
            wrap.innerHTML = '<div class="ysp-empty"><div class="ysp-empty-icon">No visits</div><div class="ysp-empty-text">Today has no appointments to checkout.</div></div>';
            return;
        }
        wrap.innerHTML = bookings.map(b => `
            <button type="button" class="ysp-client-card" data-checkout-booking="${b.id}">
                <span class="ysp-client-avatar">$</span>
                <span class="ysp-client-main">
                    <strong>${escapeHTML(b.customer_name || 'Appointment')}</strong>
                    <span>${formatTime(b.start_time)} - ${escapeHTML(b.service_name || 'Service')}</span>
                </span>
                <span class="ysp-client-meta">$${Math.abs(parseFloat(b.total_price || 0)).toFixed(2)}<br>${escapeHTML(paymentLabel(b.payment_status))}</span>
            </button>
        `).join('');
        wrap.querySelectorAll('[data-checkout-booking]').forEach(btn => {
            btn.addEventListener('click', () => {
                const booking = bookings.find(b => String(b.id) === String(btn.dataset.checkoutBooking));
                openCheckoutSheet(booking);
            });
        });
    }

    function loadLoyaltySection(booking) {
        const section = document.getElementById('ysp-loyalty-section');
        if (!section) return;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_get_loyalty_balance',
            nonce: yspData.nonce,
            phone: booking.customer_phone || '',
            email: booking.customer_email || '',
        }, function(resp) {
            if (!resp.success || !resp.data.enabled) return;
            const balance = resp.data.balance || 0;
            const rewards = resp.data.rewards || [];
            const availableIds = (resp.data.available_reward_ids || []).map(String);
            if (!rewards.length && balance <= 0) return;

            section.style.display = '';
            section.innerHTML = `
                <div class="ysp-checkout-group-title">Loyalty - ${balance} point${balance === 1 ? '' : 's'}</div>
                ${!rewards.length ? '<p class="ysp-sheet-muted">No rewards configured yet.</p>' : `
                <div class="ysp-sheet-actions ysp-checkout-actions">
                    ${rewards.map(r => {
                        const canRedeem = availableIds.includes(String(r.id));
                        const label = r.discount_type === 'percent' ? (r.discount_value + '% off') : ('$' + parseFloat(r.discount_value).toFixed(2) + ' off');
                        return `<button class="ysp-action-btn ${canRedeem ? 'call' : 'cancel'}" data-redeem-reward="${r.id}" ${canRedeem ? '' : 'disabled'}>${escapeHTML(r.name)} (${r.points_required} pts -> ${label})</button>`;
                    }).join('')}
                </div>`}
            `;
            section.querySelectorAll('[data-redeem-reward]').forEach(btn => {
                btn.addEventListener('click', () => {
                    if (!window.confirm('Redeem "' + btn.textContent.trim() + '" for this checkout?')) return;
                    btn.disabled = true;
                    jQuery.post(yspData.ajaxUrl, {
                        action: 'yatoon_redeem_loyalty_reward',
                        nonce: yspData.nonce,
                        reward_id: btn.dataset.redeemReward,
                        phone: booking.customer_phone || '',
                        email: booking.customer_email || '',
                        booking_id: booking.id,
                    }, function(r) {
                        if (r.success) {
                            toast('Reward redeemed - apply the discount manually when collecting payment.');
                            loadLoyaltySection(booking);
                        } else {
                            toast((r.data && r.data.message) || 'Could not redeem.');
                            btn.disabled = false;
                        }
                    });
                });
            });
        });
    }

    function openCheckoutSheet(booking) {
        if (!booking || !state.permissions.checkout) return;
        const payUrl = checkoutPayUrl();
        const squareNote = (yspData.squareCheckoutNote || 'Open Square App and use Tap to Pay, then return here and mark the appointment paid.').trim();
        const showNoShowTools = squareNoShowFeeEnabled() && booking.status === 'no_show';
        $('ysp-detail-overlay').classList.remove('ysp-settings-overlay');
        $('ysp-sheet-body').innerHTML = `
            <div class="ysp-checkout-top">
                <button type="button" class="ysp-detail-close" data-ysp-close="detail" aria-label="Close">&times;</button>
                <strong>Checkout</strong>
                <span aria-hidden="true"></span>
            </div>
            <div class="ysp-sheet-time ysp-checkout-title">Checkout</div>
            <div class="ysp-checkout-hero">
                <div>
                    <div class="ysp-sheet-customer">${escapeHTML(booking.customer_name || 'Appointment')}</div>
                    <div class="ysp-sheet-service">${escapeHTML(booking.service_name || 'Service')} - ${formatTime(booking.start_time)}</div>
                    <div class="ysp-status-row">${statusBadgeHTML(booking.status)} <span class="ysp-status-badge ${booking.payment_status || 'unpaid'}">${escapeHTML(paymentLabel(booking.payment_status))}</span></div>
                </div>
                <strong class="ysp-checkout-amount">$${Math.abs(parseFloat(booking.total_price || 0)).toFixed(2)}</strong>
            </div>
            <div class="ysp-checkout-help">
                <strong>In-person checkout</strong>
                <span>${escapeHTML(squareNote)}</span>
            </div>
            <div class="ysp-checkout-groups">
                <section class="ysp-checkout-group" id="ysp-loyalty-section" style="display:none;"></section>
                <section class="ysp-checkout-group">
                    <div class="ysp-checkout-group-title">Collect now</div>
                    <div class="ysp-sheet-actions ysp-checkout-actions">
                        ${yspData.squareAppUrl ? '<button class="ysp-action-btn ysp-checkout-primary" data-open-square-app="1">Open Square App</button>' : ''}
                        <button class="ysp-action-btn complete" data-checkout-op="mark_square_pos">Paid in Square POS</button>
                        <button class="ysp-action-btn call" data-checkout-op="mark_cash">Mark paid manually</button>
                    </div>
                </section>
                <section class="ysp-checkout-group">
                    <div class="ysp-checkout-group-title">Payment link</div>
                    <div class="ysp-sheet-actions ysp-checkout-actions">
                        ${payUrl ? '<button class="ysp-action-btn call" data-open-pay-link="1">Open link</button><button class="ysp-action-btn call" data-copy-pay-link="1">Copy link</button>' : '<p class="ysp-sheet-muted">No payment link configured yet.</p>'}
                        <button class="ysp-action-btn call" data-checkout-op="send_request" data-channel="email">Email link</button>
                        <button class="ysp-action-btn call" data-checkout-op="send_request" data-channel="sms">SMS link</button>
                        <button class="ysp-action-btn call" data-checkout-op="send_request" data-channel="whatsapp">WhatsApp link</button>
                    </div>
                </section>
                <section class="ysp-checkout-group ysp-checkout-danger">
                    <button class="ysp-action-btn cancel" data-checkout-op="mark_unpaid">Mark unpaid</button>
                </section>
                ${showNoShowTools ? `
                <section class="ysp-checkout-group ysp-checkout-danger">
                    <div class="ysp-checkout-group-title">Square no-show fee</div>
                    <p class="ysp-sheet-muted">${escapeHTML(yspData.squareNoShowFeeNote || 'Open Square and charge the approved no-show or late-cancellation fee, then mark it here.')}</p>
                    <div class="ysp-sheet-actions ysp-checkout-actions">
                        ${yspData.squareAppUrl ? '<button class="ysp-action-btn call" data-open-square-app="1">Open Square App</button>' : ''}
                        ${payUrl ? '<button class="ysp-action-btn call" data-open-pay-link="1">Open Square link</button>' : ''}
                        ${booking.square_booking_id ? '<button class="ysp-action-btn call" data-copy-square-booking="1">Copy Square booking ID</button>' : ''}
                        <button class="ysp-action-btn complete" data-checkout-op="mark_no_show_fee_square">Mark $${squareNoShowFeeAmount()} fee charged</button>
                        <button class="ysp-action-btn cancel" data-checkout-op="waive_no_show_fee">Waive no-show fee</button>
                    </div>
                </section>` : ''}
            </div>
        `;
        $('ysp-sheet-body').querySelectorAll('[data-checkout-op]').forEach(btn => {
            btn.addEventListener('click', () => staffCheckout(booking.id, btn.dataset.checkoutOp, btn.dataset.channel || 'email', btn));
        });
        loadLoyaltySection(booking);
        const squareAppBtn = $('ysp-sheet-body').querySelector('[data-open-square-app]');
        if (squareAppBtn) {
            squareAppBtn.addEventListener('click', () => window.open(yspData.squareAppUrl, '_blank', 'noopener'));
        }
        const openPayBtn = $('ysp-sheet-body').querySelector('[data-open-pay-link]');
        if (openPayBtn) {
            openPayBtn.addEventListener('click', () => window.open(payUrl, '_blank', 'noopener'));
        }
        const copyPayBtn = $('ysp-sheet-body').querySelector('[data-copy-pay-link]');
        if (copyPayBtn) {
            copyPayBtn.addEventListener('click', () => copyText(payUrl));
        }
        const copySquareBookingBtn = $('ysp-sheet-body').querySelector('[data-copy-square-booking]');
        if (copySquareBookingBtn) {
            copySquareBookingBtn.addEventListener('click', () => copyText(booking.square_booking_id || ''));
        }
        forceCheckoutFullscreen();
        $('ysp-detail-overlay').classList.add('open', 'ysp-checkout-overlay');
        document.body.classList.add('ysp-overlay-lock');
    }

    function openNoShowFeeSheet(booking) {
        if (!booking || !squareNoShowFeeEnabled() || !state.permissions.checkout) return false;
        booking.status = 'no_show';
        openCheckoutSheet(booking);
        toast('No-show marked. Decide whether to charge the Square fee.', 3200);
        return true;
    }

    function forceCheckoutFullscreen() {
        const overlay = $('ysp-detail-overlay');
        const sheet = $('ysp-detail-sheet');
        const body = $('ysp-sheet-body');
        if (!overlay || !sheet || !body) return;
        Object.assign(overlay.style, {
            position: 'fixed',
            inset: '0',
            left: '0',
            right: '0',
            top: '0',
            bottom: '0',
            width: '100vw',
            maxWidth: 'none',
            padding: '0',
            alignItems: 'stretch',
            justifyContent: 'stretch',
            transform: 'none',
        });
        Object.assign(sheet.style, {
            position: 'fixed',
            inset: '0',
            left: '0',
            right: '0',
            top: '0',
            bottom: '0',
            width: '100vw',
            maxWidth: 'none',
            height: '100dvh',
            maxHeight: 'none',
            minHeight: '100dvh',
            margin: '0',
            borderRadius: '0',
            transform: 'none',
            overflow: 'hidden',
        });
        Object.assign(body.style, {
            width: '100%',
            maxWidth: 'none',
            height: '100dvh',
            maxHeight: 'none',
            margin: '0',
            overflowX: 'hidden',
            overflowY: 'auto',
        });
    }

    function clearCheckoutFullscreen() {
        const overlay = $('ysp-detail-overlay');
        const sheet = $('ysp-detail-sheet');
        const body = $('ysp-sheet-body');
        [overlay, sheet, body].forEach(el => {
            if (!el) return;
            el.removeAttribute('style');
        });
    }

    function staffCheckout(bookingId, checkoutOp, channel, btn) {
        if (btn) btn.disabled = true;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_checkout',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            booking_id: bookingId,
            checkout_op: checkoutOp,
            channel,
        }, function(resp) {
            if (btn) btn.disabled = false;
            if (resp.success) {
                if (resp.data.whatsapp_url) {
                    window.open(resp.data.whatsapp_url, '_blank', 'noopener');
                }
                toast(resp.data.message || 'Checkout updated');
                closeDetail();
                loadSchedule(true);
                if (state.currentView === 'checkout') {
                    setTimeout(renderCheckoutView, 300);
                }
            } else {
                toast(resp.data && resp.data.message ? resp.data.message : 'Checkout failed');
            }
        }).fail(function() {
            if (btn) btn.disabled = false;
            toast('Network error');
        });
    }

    function openMessageComposer(client) {
        if (!client || !state.permissions.send_messages) return;
        state.messageBookingId = client.booking_id || 0;
        const defaultMsg = `Hi ${client.customer_name || ''}, this is ${yspData.bizName || 'Yatoon'} about your appointment.`;
        $('ysp-editor-body').innerHTML = `
            <div class="ysp-sheet-time">Message</div>
            <div class="ysp-sheet-customer">${escapeHTML(client.customer_name || 'Unnamed client')}</div>
            <div class="ysp-sheet-service">${escapeHTML(client.customer_phone || '')}</div>
            <div class="ysp-message-template-row">
                <button type="button" data-message-template="Hi ${escapeHTML(client.customer_name || '')}, this is ${escapeHTML(yspData.bizName || 'Yatoon')} confirming your appointment.">Confirm</button>
                <button type="button" data-message-template="Hi ${escapeHTML(client.customer_name || '')}, this is ${escapeHTML(yspData.bizName || 'Yatoon')}. We have an opening soon if you would like to book.">Opening</button>
                <button type="button" data-message-template="Hi ${escapeHTML(client.customer_name || '')}, this is ${escapeHTML(yspData.bizName || 'Yatoon')}. Thank you for visiting today.">Thanks</button>
            </div>
            <label class="ysp-editor-label" for="ysp-message-body">Message</label>
            <textarea id="ysp-message-body" class="ysp-editor-textarea" rows="5">${escapeHTML(defaultMsg)}</textarea>
            <div class="ysp-sheet-actions">
                <button class="ysp-action-btn complete" id="ysp-send-sms">Send SMS</button>
                <button class="ysp-action-btn call" id="ysp-send-whatsapp">WhatsApp</button>
                <button class="ysp-action-btn cancel" id="ysp-cancel-message">Close</button>
            </div>
        `;
        $('ysp-editor-overlay').classList.add('open');
        $('ysp-editor-body').querySelectorAll('[data-message-template]').forEach(btn => {
            btn.addEventListener('click', function() {
                $('ysp-message-body').value = this.dataset.messageTemplate || '';
            });
        });
        $('ysp-cancel-message').addEventListener('click', closeEditor);
        $('ysp-send-sms').addEventListener('click', () => sendClientMessage(client.id, 'sms'));
        $('ysp-send-whatsapp').addEventListener('click', () => sendClientMessage(client.id, 'whatsapp'));
    }

    function sendClientMessage(clientId, channel) {
        const btn = channel === 'sms' ? $('ysp-send-sms') : $('ysp-send-whatsapp');
        btn.disabled = true;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_send_message',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            client_id: clientId,
            booking_id: state.messageBookingId || 0,
            channel,
            message: $('ysp-message-body').value,
        }, function(resp) {
            btn.disabled = false;
            if (resp.success) {
                if (resp.data.whatsapp_url) {
                    window.open(resp.data.whatsapp_url, '_blank', 'noopener');
                }
                toast(resp.data.message || 'Message ready');
            } else {
                toast(resp.data && resp.data.message ? resp.data.message : 'Could not send message');
            }
        }).fail(function() {
            btn.disabled = false;
            toast('Network error');
        });
    }

    /* Detail sheet */
    function openDesignPreview(url) {
        if (!url) return;
        const existing = document.querySelector('.ysp-design-lightbox');
        if (existing) existing.remove();
        const safeUrl = escapeHTML(url);
        const lightbox = document.createElement('div');
        lightbox.className = 'ysp-design-lightbox';
        lightbox.innerHTML = `
            <div class="ysp-design-lightbox-panel" role="dialog" aria-modal="true" aria-label="Design reference image">
                <button type="button" class="ysp-design-lightbox-close" aria-label="Close">&times;</button>
                <img src="${safeUrl}" alt="Customer design reference">
                <a href="${safeUrl}" target="_blank" rel="noopener">Open original</a>
            </div>
        `;
        document.body.appendChild(lightbox);
        const close = () => lightbox.remove();
        lightbox.addEventListener('click', e => {
            if (e.target === lightbox || e.target.classList.contains('ysp-design-lightbox-close')) close();
        });
        document.addEventListener('keydown', function escHandler(e) {
            if (e.key === 'Escape') {
                close();
                document.removeEventListener('keydown', escHandler);
            }
        });
    }

    function openGroupEditPicker(booking) {
        const items = booking._groupBookings.map(sub => {
            const subDuration = Math.round((new Date(sub.end_time) - new Date(sub.start_time)) / 60000);
            return `
                <button type="button" class="ysp-client-card" data-edit-sub="${sub.id}">
                    <span class="ysp-client-avatar">${formatTime(sub.start_time).replace(':00', '')}</span>
                    <span class="ysp-client-main">
                        <strong>${escapeHTML(sub.service_name || 'Service')}</strong>
                        <span>${subDuration} min - $${Math.abs(parseFloat(sub.total_price || 0)).toFixed(2)}</span>
                    </span>
                    <span class="ysp-client-meta">Edit</span>
                </button>
            `;
        }).join('');

        $('ysp-sheet-body').innerHTML = `
            <div class="ysp-detail-top">
                <button type="button" class="ysp-detail-close" data-ysp-close="detail" aria-label="Close">&times;</button>
            </div>
            <h2 style="margin:4px 0 4px;">Which service?</h2>
            <p style="color:#6b7280;font-size:13px;margin:0 0 16px;">This visit has ${booking._groupBookings.length} services. Pick one to edit its time, staff, or notes.</p>
            <div class="ysp-client-list">${items}</div>
            <div class="ysp-sheet-actions" style="margin-top:16px;">
                <button class="ysp-action-btn cancel" data-back-to-detail="1">Back</button>
            </div>
        `;
        $('ysp-sheet-body').querySelectorAll('[data-edit-sub]').forEach(btn => {
            btn.addEventListener('click', () => {
                const sub = booking._groupBookings.find(s => String(s.id) === String(btn.dataset.editSub));
                if (sub) openBookingEditor(sub);
            });
        });
        const backBtn = $('ysp-sheet-body').querySelector('[data-back-to-detail]');
        if (backBtn) backBtn.addEventListener('click', () => openDetail(booking));
    }

    function openDetail(booking) {
        state.activeBooking = booking;
        const duration = Math.round((new Date(booking.end_time) - new Date(booking.start_time)) / 60000);
        const endTime = formatTime(booking.end_time);
        const staffLabel = state.permissions.view_all_schedules && booking.staff_name ? ' - ' + escapeHTML(booking.staff_name) : '';
        const startTime = new Date(booking.start_time);
        const showNoShow = Date.now() >= startTime.getTime();
        const bookingDate = booking._date || state.weekDays[state.tab];
        const editAction = state.permissions.edit_booking ? '<button type="button" class="ysp-detail-edit" data-edit-booking="1">Edit</button>' : '';
        const paymentDue = Math.abs(parseFloat(booking.total_price || 0)).toFixed(2);
        const statusActions = state.permissions.update_status ? `
            <div class="ysp-square-section ysp-detail-actions">
                ${state.permissions.checkout ? '<button class="ysp-square-primary" data-checkout-booking="1">Review & checkout</button>' : ''}
                <button class="ysp-square-row-action" data-action="arrived"><span>Mark arrived</span><strong>Arrived</strong></button>
                <button class="ysp-square-row-action" data-action="completed"><span>Finish visit</span><strong>Complete</strong></button>
                ${showNoShow ? '<button class="ysp-square-row-action danger" data-action="no_show"><span>Appointment time has started</span><strong>No-show</strong></button>' : ''}
            </div>
        ` : '';

        $('ysp-sheet-body').innerHTML = `
            <div class="ysp-detail-top">
                <button type="button" class="ysp-detail-close" data-ysp-close="detail" aria-label="Close">&times;</button>
                ${editAction}
            </div>
            <div class="ysp-detail-status">Accepted</div>
            <div class="ysp-detail-due">$${paymentDue} due</div>
            ${state.permissions.checkout ? '<button class="ysp-square-primary ysp-detail-checkout" data-checkout-booking="1">Review & checkout</button>' : ''}

            <section class="ysp-square-section">
                <h3>Customer</h3>
                <button type="button" class="ysp-square-customer" data-open-client-profile="1">
                    <span><strong>${escapeHTML(booking.customer_name || 'Appointment')}</strong><em>${escapeHTML([booking.customer_phone, paymentLabel(booking.payment_status)].filter(Boolean).join(' - '))}</em></span>
                    <b aria-hidden="true">?</b>
                </button>
                <div class="ysp-status-row">${statusBadgeHTML(booking.status)} <span class="ysp-status-badge ${booking.payment_status || 'unpaid'}">${escapeHTML(paymentLabel(booking.payment_status))}</span></div>
            </section>

            <section class="ysp-square-section">
                <h3>Services and items</h3>
                ${booking._isGroup ? (() => {
                    // Each line shows: bold service name, a subtitle for any
                    // selected option/variant (e.g. "With Gel Polish" under
                    // "Luxe Pedicure" instead of replacing the service name
                    // entirely), and a start time computed as a running
                    // offset from the visit's own start time using each
                    // item's duration -- rather than trusting each row's
                    // stored start_time, which is not always staggered
                    // depending on how the booking was created or synced.
                    let runningStart = new Date(booking.start_time);
                    return booking._groupBookings.map(sub => {
                        const subDuration = Math.round((new Date(sub.end_time) - new Date(sub.start_time)) / 60000) || duration;
                        const itemStart = new Date(runningStart);
                        runningStart = new Date(runningStart.getTime() + subDuration * 60000);
                        const subStaffLabel = state.permissions.view_all_schedules && sub.staff_name ? `, ${escapeHTML(sub.staff_name)}` : '';
                        return `
                        <div class="ysp-square-line">
                            <span>
                                <strong>${escapeHTML(sub.service_name || 'Service')}</strong>
                                ${sub.option_names ? `<em>${escapeHTML(sub.option_names)}</em>` : ''}
                                <em>${formatTime(itemStart)} &middot; ${subDuration} min${subStaffLabel}</em>
                            </span>
                            <strong>$${Math.abs(parseFloat(sub.total_price || 0)).toFixed(2)}</strong>
                        </div>
                    `; }).join('');
                })() : `
                <div class="ysp-square-line">
                    <span>
                        <strong>${escapeHTML(booking.service_name || 'Service')}</strong>
                        ${booking.option_names ? `<em>${escapeHTML(booking.option_names)}</em>` : ''}
                        <em>${duration} min${staffLabel}</em>
                    </span>
                    <strong>$${paymentDue}</strong>
                </div>`}
                <div class="ysp-square-total"><strong>Total</strong><strong>$${booking._isGroup ? booking._groupBookings.reduce((sum, sub) => sum + Math.abs(parseFloat(sub.total_price || 0)), 0).toFixed(2) : paymentDue}</strong></div>
            </section>

            <section class="ysp-square-section">
                <h3>Date and time</h3>
                <p><strong>${formatDate(bookingDate)}</strong><br><span>${formatTime(booking.start_time)} - ${endTime} (${duration} min)</span></p>
                ${booking.created_at ? `<p class="ysp-booked-meta"><em>Booked on ${escapeHTML(formatDateValue(booking.created_at))} ${escapeHTML(formatTime(booking.created_at))}</em></p>` : ''}
            </section>

            <section class="ysp-square-section">
                <h3>Staff</h3>
                <p><strong>${escapeHTML(booking.staff_name || state.staffName || 'Staff')}</strong></p>
            </section>

            ${(booking.client_allergies || booking.client_preferences || booking.client_notes || parseInt(booking.client_booking_count || 0, 10)) ? `
            <section class="ysp-square-section ysp-client-context">
                <h3>Client context</h3>
                <div class="ysp-client-context-grid">
                    <div><span>Visit history</span><strong>${parseInt(booking.client_booking_count || 0, 10)} appointment${parseInt(booking.client_booking_count || 0, 10) === 1 ? '' : 's'}</strong>${booking.client_last_booking ? `<em>Last visit ${new Date(Number(booking.client_last_booking)).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</em>` : ''}</div>
                    ${booking.client_allergies ? `<div class="attention"><span>Allergies</span><strong>${escapeHTML(booking.client_allergies)}</strong></div>` : ''}
                    ${booking.client_preferences ? `<div><span>Preferences</span><strong>${escapeHTML(booking.client_preferences)}</strong></div>` : ''}
                    ${booking.client_notes ? `<div><span>Recent notes</span><strong>${escapeHTML(booking.client_notes)}</strong></div>` : ''}
                </div>
            </section>` : ''}

            ${booking.notes ? `<section class="ysp-square-section"><h3>Appointment note</h3><div class="ysp-square-note">${escapeHTML(booking.notes)}</div></section>` : ''}

            ${booking.design_image_url ? `
            <section class="ysp-square-section ysp-design-reference">
                <h3>Design reference</h3>
                <button type="button" data-preview-design="${escapeHTML(booking.design_image_url)}">
                    <img src="${escapeHTML(booking.design_image_url)}" alt="Customer design reference">
                    <span>Preview full-size image</span>
                </button>
            </section>` : ''}

            ${statusActions}

            <div class="ysp-square-bottom-actions">
                ${booking.customer_phone ? '<button class="ysp-square-secondary" data-call-customer="1">Call customer</button>' : ''}
                ${booking.customer_phone && state.permissions.send_messages ? '<button class="ysp-square-secondary" data-message-booking="1">Message customer</button>' : ''}
                ${state.permissions.cancel_booking ? '<button class="ysp-square-secondary danger" data-cancel-booking="1">Cancel appointment</button>' : ''}
            </div>
        `;

        $('ysp-sheet-body').querySelectorAll('[data-action]').forEach(btn => {
            btn.addEventListener('click', () => {
                if (btn.dataset.action === 'no_show' && !window.confirm('Mark this appointment as no-show?')) {
                    return;
                }
                const ids = booking._isGroup ? booking._groupBookings.map(s => s.id) : booking.id;
                updateStatus(ids, btn.dataset.action);
            });
        });
        const profileBtn = $('ysp-sheet-body').querySelector('[data-open-client-profile]');
        if (profileBtn) profileBtn.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();
            if (!state.permissions.view_clients) {
                showClientAccessDenied();
                return;
            }
            openClientDetail(clientFromBooking(booking));
        });
        $('ysp-sheet-body').querySelectorAll('[data-call-customer]').forEach(btn => {
            btn.addEventListener('click', () => {
                if (booking.customer_phone) window.location.href = 'tel:' + booking.customer_phone;
            });
        });
        const editBtn = $('ysp-sheet-body').querySelector('[data-edit-booking]');
        if (editBtn) editBtn.addEventListener('click', () => {
            if (booking._isGroup) {
                openGroupEditPicker(booking);
            } else {
                openBookingEditor(booking);
            }
        });
        const designPreviewBtn = $('ysp-sheet-body').querySelector('[data-preview-design]');
        if (designPreviewBtn) designPreviewBtn.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();
            openDesignPreview(designPreviewBtn.dataset.previewDesign || '');
        });
        const cancelBtn = $('ysp-sheet-body').querySelector('[data-cancel-booking]');
        if (cancelBtn) cancelBtn.addEventListener('click', () => cancelBooking(booking.id));
        $('ysp-sheet-body').querySelectorAll('[data-checkout-booking]').forEach(btn => {
            btn.addEventListener('click', e => {
                e.preventDefault();
                e.stopPropagation();
                openCheckoutSheet(booking);
            });
        });
        const messageBookingBtn = $('ysp-sheet-body').querySelector('[data-message-booking]');
        if (messageBookingBtn) {
            messageBookingBtn.addEventListener('click', () => openMessageComposer({
                id: 0,
                booking_id: booking.id,
                customer_name: booking.customer_name,
                customer_phone: booking.customer_phone,
                customer_email: booking.customer_email,
            }));
        }

        $('ysp-detail-overlay').classList.add('open');
        document.body.classList.add('ysp-overlay-lock');
    }

    function closeDetail() {
        $('ysp-detail-overlay').classList.remove('open');
        $('ysp-detail-overlay').classList.remove('ysp-settings-overlay');
        $('ysp-detail-overlay').classList.remove('ysp-checkout-overlay');
        clearCheckoutFullscreen();
        document.body.classList.remove('ysp-overlay-lock');
        state.activeBooking = null;
        setBottomNavActive(state.currentView);
    }

    function closeEditor() {
        $('ysp-editor-overlay').classList.remove('open');
    }

    function renderMoreMenu() {
        document.querySelectorAll('.ysp-bottom-nav-btn').forEach(btn => btn.classList.remove('active'));
        if ($('ysp-bottom-more-btn')) $('ysp-bottom-more-btn').classList.add('active');
        $('ysp-sheet-body').innerHTML = `
            <div class="ysp-settings-panel">
                <div class="ysp-settings-head">
                    <button type="button" class="ysp-settings-close" data-ysp-close="detail" aria-label="Close">&times;</button>
                    <strong>More</strong>
                    <span></span>
                </div>
                <section class="ysp-more-actions">
                    <button type="button" data-more-action="profile"><span>My profile</span><b>Name, bio and calendar color</b></button>
                    <button type="button" data-more-action="calendar"><span>Calendar settings</span><b>View and employee filters</b></button>
                    ${(state.permissions.request_time_off || state.permissions.approve_time_off) ? '<button type="button" data-more-action="timeoff"><span>Time off</span><b>Requests and approvals</b></button>' : ''}
                    <button type="button" data-more-action="refresh"><span>Refresh</span><b>Sync the latest appointments</b></button>
                    <button type="button" data-more-action="logout" class="danger"><span>Sign out</span><b>End this staff session</b></button>
                </section>
            </div>
        `;
        $('ysp-detail-overlay').classList.add('open', 'ysp-settings-overlay');
        document.body.classList.add('ysp-overlay-lock');
        $('ysp-sheet-body').querySelectorAll('[data-more-action]').forEach(btn => {
            btn.addEventListener('click', () => {
                const action = btn.dataset.moreAction;
                if (action === 'profile') renderStaffProfile();
                if (action === 'calendar') renderCalendarSettings();
                if (action === 'timeoff') renderTimeOff();
                if (action === 'refresh') {
                    closeDetail();
                    loadSchedule(false);
                }
                if (action === 'logout') {
                    closeDetail();
                    logout();
                }
            });
        });
    }

    function renderStaffProfileError(message) {
        $('ysp-sheet-body').innerHTML = `
            <div class="ysp-settings-panel">
                <div class="ysp-settings-head">
                    <button type="button" class="ysp-settings-close" data-ysp-close="detail" aria-label="Close">&times;</button>
                    <strong>My profile</strong><span></span>
                </div>
                <p class="ysp-sheet-muted">${escapeHTML(message || 'Unable to load profile')}</p>
                <div class="ysp-sheet-actions"><button type="button" class="ysp-action-btn primary" id="ysp-profile-retry">Try again</button></div>
            </div>`;
        $('ysp-profile-retry').addEventListener('click', renderStaffProfile);
    }

    function renderStaffProfile() {
        $('ysp-sheet-body').innerHTML = '<div class="ysp-loading-wrap"><div class="ysp-spinner"></div></div>';
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_get_profile',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
        }, function(resp) {
            if (!resp.success) {
                renderStaffProfileError((resp.data && resp.data.message) || 'Unable to load profile');
                return;
            }
            const profile = resp.data.profile || {};
            $('ysp-sheet-body').innerHTML = `
                <div class="ysp-settings-panel">
                    <div class="ysp-settings-head">
                        <button type="button" class="ysp-settings-close" data-ysp-close="detail" aria-label="Close">&times;</button>
                        <strong>My profile</strong><span></span>
                    </div>
                    <label class="ysp-editor-label" for="ysp-profile-name">Display name</label>
                    <input class="ysp-editor-input" id="ysp-profile-name" maxlength="120" value="${escapeHTML(profile.name || '')}">
                    <label class="ysp-editor-label" for="ysp-profile-bio">Bio or specialty</label>
                    <textarea class="ysp-editor-textarea" id="ysp-profile-bio" rows="5" maxlength="1000">${escapeHTML(profile.bio || '')}</textarea>
                    <label class="ysp-editor-label" for="ysp-profile-color">Calendar color</label>
                    <input class="ysp-editor-input ysp-profile-color" id="ysp-profile-color" type="color" value="${escapeHTML(profile.calendar_color || '#ec4899')}">
                    <p class="ysp-sheet-muted">Your email and access permissions are managed by an administrator.</p>
                    <div class="ysp-sheet-actions"><button type="button" class="ysp-action-btn primary" id="ysp-profile-save">Save profile</button></div>
                </div>`;
            $('ysp-profile-save').addEventListener('click', function() {
                const button = this;
                const originalLabel = button.textContent;
                button.disabled = true;
                button.textContent = 'Saving...';
                jQuery.post(yspData.ajaxUrl, {
                    action: 'yatoon_staff_save_profile',
                    nonce: yspData.nonce,
                    staff_id: state.staffId,
                    portal_token: state.portalToken,
                    name: $('ysp-profile-name').value,
                    bio: $('ysp-profile-bio').value,
                    calendar_color: $('ysp-profile-color').value,
                }, function(saveResp) {
                    button.disabled = false;
                    button.textContent = originalLabel;
                    if (!saveResp.success) {
                        toast((saveResp.data && saveResp.data.message) || 'Unable to save profile');
                        return;
                    }
                    state.staffName = saveResp.data.profile.name;
                    if ($('ysp-staff-name')) $('ysp-staff-name').textContent = state.staffName;
                    toast(saveResp.data.message || 'Profile saved');
                    closeDetail();
                }).fail(function() {
                    button.disabled = false;
                    button.textContent = originalLabel;
                    toast('Network error');
                });
            });
        }).fail(function() {
            renderStaffProfileError('Network error. Check your connection and try again.');
        });
    }

    function renderTimeOff() {
        $('ysp-sheet-body').innerHTML = '<div class="ysp-loading-wrap"><div class="ysp-spinner"></div></div>';
        $('ysp-detail-overlay').classList.add('open', 'ysp-settings-overlay');
        document.body.classList.add('ysp-overlay-lock');
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_get_time_off',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
        }, function(resp) {
            if (!resp.success) {
                toast(resp.data && resp.data.message ? resp.data.message : 'Could not load time off');
                return;
            }
            const requests = resp.data.requests || [];
            const canApprove = !!resp.data.can_approve;
            const tomorrow = new Date();
            tomorrow.setDate(tomorrow.getDate() + 1);
            const defaultDate = tomorrow.getFullYear() + '-' + String(tomorrow.getMonth() + 1).padStart(2, '0') + '-' + String(tomorrow.getDate()).padStart(2, '0');
            $('ysp-sheet-body').innerHTML = `
                <div class="ysp-settings-panel ysp-timeoff-panel">
                    <div class="ysp-settings-head">
                        <button type="button" class="ysp-settings-close" data-ysp-close="detail" aria-label="Close">&times;</button>
                        <strong>Time off</strong>
                        <span></span>
                    </div>
                    ${state.permissions.request_time_off ? `
                    <section>
                        <h3>New request</h3>
                        <label class="ysp-editor-label" for="ysp-timeoff-date">Date</label>
                        <input class="ysp-editor-input" id="ysp-timeoff-date" type="date" value="${defaultDate}">
                        <label class="ysp-timeoff-all"><input id="ysp-timeoff-all-day" type="checkbox"> All day</label>
                        <div class="ysp-editor-grid" id="ysp-timeoff-hours">
                            <div><label class="ysp-editor-label" for="ysp-timeoff-start">Start</label><input class="ysp-editor-input" id="ysp-timeoff-start" type="time" value="09:00"></div>
                            <div><label class="ysp-editor-label" for="ysp-timeoff-end">End</label><input class="ysp-editor-input" id="ysp-timeoff-end" type="time" value="17:00"></div>
                        </div>
                        <label class="ysp-editor-label" for="ysp-timeoff-reason">Reason</label>
                        <input class="ysp-editor-input" id="ysp-timeoff-reason" type="text" maxlength="255" placeholder="Vacation, appointment, personal">
                        <button type="button" class="ysp-small-primary ysp-timeoff-submit" id="ysp-timeoff-submit">Send request</button>
                    </section>` : ''}
                    <section>
                        <h3>${canApprove ? 'Requests to review' : 'My requests'}</h3>
                        <div class="ysp-timeoff-list">
                            ${requests.length ? requests.map(r => `
                                <article class="ysp-timeoff-request ${escapeHTML(r.status)}">
                                    <div><strong>${canApprove ? escapeHTML(r.staff_name || 'Staff') : escapeHTML(String(r.start_at).slice(0, 10))}</strong><span>${canApprove ? escapeHTML(String(r.start_at).slice(0, 10)) + ' - ' : ''}${parseInt(r.is_all_day || 0, 10) ? 'All day' : escapeHTML(String(r.start_at).slice(11, 16) + ' - ' + String(r.end_at).slice(11, 16))}</span></div>
                                    <p>${escapeHTML(r.reason || 'No reason provided')}</p>
                                    <span class="ysp-timeoff-status">${escapeHTML(r.status)}</span>
                                    ${canApprove && r.status === 'pending' ? `<div class="ysp-timeoff-review"><button type="button" data-review="${r.id}" data-decision="approved">Approve</button><button type="button" data-review="${r.id}" data-decision="rejected" class="danger">Reject</button></div>` : ''}
                                </article>
                            `).join('') : '<div class="ysp-today-open"><strong>No requests</strong><span>Time off requests will appear here.</span></div>'}
                        </div>
                    </section>
                </div>
            `;
            const allDay = $('ysp-timeoff-all-day');
            if (allDay) allDay.addEventListener('change', () => {
                $('ysp-timeoff-hours').style.display = allDay.checked ? 'none' : '';
            });
            const submit = $('ysp-timeoff-submit');
            if (submit) submit.addEventListener('click', () => submitTimeOff(submit));
            $('ysp-sheet-body').querySelectorAll('[data-review]').forEach(btn => {
                btn.addEventListener('click', () => reviewTimeOff(btn.dataset.review, btn.dataset.decision, btn));
            });
        }).fail(function() {
            toast('Network error');
        });
    }

    function submitTimeOff(button) {
        button.disabled = true;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_request_time_off',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            date: $('ysp-timeoff-date').value,
            all_day: $('ysp-timeoff-all-day').checked ? 1 : 0,
            start: $('ysp-timeoff-start').value,
            end: $('ysp-timeoff-end').value,
            reason: $('ysp-timeoff-reason').value.trim(),
        }, function(resp) {
            button.disabled = false;
            toast(resp.success ? (resp.data.message || 'Request sent') : (resp.data && resp.data.message ? resp.data.message : 'Could not send request'));
            if (resp.success) renderTimeOff();
        }).fail(function() {
            button.disabled = false;
            toast('Network error');
        });
    }

    function reviewTimeOff(requestId, decision, button) {
        button.disabled = true;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_review_time_off',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            portal_token: state.portalToken,
            request_id: requestId,
            decision,
        }, function(resp) {
            toast(resp.success ? (resp.data.message || 'Updated') : (resp.data && resp.data.message ? resp.data.message : 'Could not update request'));
            if (resp.success) renderTimeOff();
            else button.disabled = false;
        }).fail(function() {
            button.disabled = false;
            toast('Network error');
        });
    }

    function renderCalendarSettings() {
        const staff = state.permissions.view_all_schedules && state.staffRoster.length ? state.staffRoster : [{ id: String(state.staffId), name: state.staffName || 'Staff', color: '' }];
        const selected = state.selectedStaffIds && state.selectedStaffIds.length ? state.selectedStaffIds.map(String) : staff.map(s => String(s.id));
        $('ysp-sheet-body').innerHTML = `
            <div class="ysp-settings-panel">
                <div class="ysp-settings-head">
                    <button type="button" class="ysp-settings-close" data-ysp-close="detail" aria-label="Close">&times;</button>
                    <strong>Calendar</strong>
                    <button type="button" class="ysp-settings-done" data-ysp-close="detail">Done</button>
                </div>
                <section>
                    <h3>Calendar view</h3>
                    ${['month', 'day', 'week', 'list'].map(mode => `
                        <label class="ysp-settings-row">
                            <span>${mode === 'list' ? 'List' : mode.charAt(0).toUpperCase() + mode.slice(1)}</span>
                            <input type="radio" name="ysp-calendar-mode" value="${mode}" ${state.calendarMode === mode ? 'checked' : ''}>
                        </label>
                    `).join('')}
                </section>
                <section>
                    <h3>Employees</h3>
                    ${staff.map((s, i) => `
                        <label class="ysp-settings-row employee">
                            <span><i style="background:${validStaffColor(s.color) || ['#e58b16', '#84cc16', '#a855f7', '#1677ff', '#22c55e'][i % 5]}"></i>${escapeHTML(s.name)}</span>
                            <input type="checkbox" value="${escapeHTML(s.id)}" ${selected.includes(String(s.id)) ? 'checked' : ''}>
                        </label>
                    `).join('')}
                </section>
            </div>
        `;
        $('ysp-detail-overlay').classList.add('open', 'ysp-settings-overlay');
        document.body.classList.add('ysp-overlay-lock');
        $('ysp-sheet-body').querySelectorAll('input[name="ysp-calendar-mode"]').forEach(input => {
            input.addEventListener('change', () => {
                setCalendarMode(input.value);
            });
        });
        $('ysp-sheet-body').querySelectorAll('.ysp-settings-row.employee input').forEach(input => {
            input.addEventListener('change', () => {
                const mobile = window.matchMedia && window.matchMedia('(max-width: 640px)').matches;
                if (mobile && input.checked) {
                    $('ysp-sheet-body').querySelectorAll('.ysp-settings-row.employee input').forEach(el => {
                        if (el !== input) el.checked = false;
                    });
                }
                const checked = Array.from($('ysp-sheet-body').querySelectorAll('.ysp-settings-row.employee input:checked')).map(el => el.value);
                state.selectedStaffIds = checked.length === staff.length ? null : checked;
                renderStaffTabs();
                renderDayTabs();
                renderSchedule();
            });
        });
    }

    function editorField(id, label, type, value) {
        return `<label class="ysp-editor-label" for="${id}">${label}</label>
            <input id="${id}" class="ysp-editor-input" type="${type}" value="${escapeHTML(value || '')}">`;
    }

    function loadEditorData(done) {
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_get_booking_form_data',
            nonce: yspData.nonce,
            staff_id: state.staffId,
        }, function(resp) {
            if (!resp.success) {
                toast(resp.data && resp.data.message ? resp.data.message : 'Could not load form');
                return;
            }
            done(resp.data || {});
        }).fail(function() {
            toast('Network error');
        });
    }

    function openBookingEditor(booking, preset = {}) {
        if (booking && !state.permissions.edit_booking) {
            toast('You do not have permission to edit appointments.');
            return;
        }
        if (!booking && !state.permissions.create_booking) {
            toast('You do not have permission to add appointments.');
            return;
        }
        loadEditorData(function(data) {
            const services = data.services || [];
            const staff = data.staff || [];
            const start = booking ? new Date(booking.start_time) : (preset.start_time ? new Date(preset.start_time) : new Date());
            const dateVal = start.getFullYear() + '-' + String(start.getMonth() + 1).padStart(2, '0') + '-' + String(start.getDate()).padStart(2, '0');
            const timeVal = String(start.getHours()).padStart(2, '0') + ':' + String(start.getMinutes()).padStart(2, '0');
            const serviceOptions = services.map(s => `<option value="${s.id}" data-duration="${s.duration_minutes}" ${booking && String(booking.service_id) === String(s.id) ? 'selected' : ''}>${escapeHTML(s.category ? s.category + ' - ' + s.name : s.name)}</option>`).join('');
            const selectedOptionIds = booking && booking.option_ids ? String(booking.option_ids).split(',').map(String) : [];
            const optionPicker = (serviceId) => {
                const svc = services.find(s => String(s.id) === String(serviceId));
                const opts = svc && Array.isArray(svc.options) ? svc.options : [];
                if (!opts.length) return '<p class="ysp-editor-help">No variations or add-ons for this service.</p>';
                return opts.map(o => {
                    const isAddon = String(o.group_name || '') === 'Add-on';
                    const checked = selectedOptionIds.includes(String(o.id));
                    return `<label class="ysp-service-option ${isAddon ? 'is-addon' : 'is-main'}">
                        <input type="${isAddon ? 'checkbox' : 'radio'}" name="${isAddon ? 'ysp-addon-options[]' : 'ysp-main-option'}" value="${o.id}" data-duration="${o.duration_minutes || 0}" ${checked ? 'checked' : ''}>
                        <span><strong>${escapeHTML(o.name)}</strong><small>${isAddon ? 'Add-on · ' : ''}${parseInt(o.duration_minutes || 0, 10)} min</small></span>
                    </label>`;
                }).join('');
            };
            const selectedStaff = booking ? booking.staff_id : (preset.staff_id || state.staffId);
            const staffOptions = staff.map(s => `<option value="${s.id}" ${String(selectedStaff) === String(s.id) ? 'selected' : ''}>${escapeHTML(s.name)}</option>`).join('');

            $('ysp-editor-body').innerHTML = `
                <div class="ysp-sheet-time">${booking ? 'Edit appointment' : 'Add appointment'}</div>
                <div class="ysp-editor-section-title">Customer</div>
                ${editorField('ysp-customer-search', 'Search existing client', 'search', '')}
                <div id="ysp-client-results" class="ysp-client-results"></div>
                ${editorField('ysp-edit-name', 'Customer name', 'text', booking ? booking.customer_name : '')}
                <div class="ysp-editor-grid">
                    <div>${editorField('ysp-edit-phone', 'Phone', 'tel', booking ? booking.customer_phone : '')}</div>
                    <div>${editorField('ysp-edit-email', 'Email', 'email', booking ? booking.customer_email : '')}</div>
                </div>
                <label class="ysp-editor-label" for="ysp-edit-service">Service</label>
                <select id="ysp-edit-service" class="ysp-editor-select">${serviceOptions}</select>
                <div class="ysp-editor-section-title">Service details</div>
                <div id="ysp-edit-options" class="ysp-service-options">${optionPicker(booking ? booking.service_id : (services[0] && services[0].id))}</div>
                <label class="ysp-editor-label" for="ysp-edit-staff">Staff</label>
                <select id="ysp-edit-staff" class="ysp-editor-select">${staffOptions}</select>
                <div class="ysp-editor-grid">
                    <div>${editorField('ysp-edit-date', 'Date', 'date', dateVal)}</div>
                    <div>${editorField('ysp-edit-time', 'Time', 'time', timeVal)}</div>
                </div>
                <label class="ysp-editor-label" for="ysp-edit-duration">Duration (minutes)</label>
                <input id="ysp-edit-duration" class="ysp-editor-input" type="number" min="5" max="600" step="5" value="${(function () {
                    if (booking) return Math.max(5, Math.round((new Date(booking.end_time) - new Date(booking.start_time)) / 60000));
                    const sel = services.find(s => String(s.id) === String(services[0] && services[0].id));
                    return sel ? sel.duration_minutes : 60;
                })()}">
                <label class="ysp-editor-label" for="ysp-edit-notes">Notes</label>
                <textarea id="ysp-edit-notes" class="ysp-editor-textarea" rows="3">${escapeHTML(booking ? booking.notes : '')}</textarea>
                <div class="ysp-sheet-actions">
                    <button class="ysp-action-btn complete" id="ysp-save-booking">${booking ? 'Save changes' : 'Add appointment'}</button>
                    <button class="ysp-action-btn cancel" id="ysp-cancel-editor">Close</button>
                </div>
            `;
            $('ysp-editor-overlay').classList.add('open');
            $('ysp-cancel-editor').addEventListener('click', closeEditor);
            $('ysp-save-booking').addEventListener('click', function() { saveBooking(booking ? booking.id : 0); });
            $('ysp-customer-search').addEventListener('input', debounce(searchClients, 250));
            $('ysp-edit-service').addEventListener('change', function() {
                const opt = this.options[this.selectedIndex];
                const dur = opt ? parseInt(opt.dataset.duration, 10) : 0;
                if (dur) $('ysp-edit-duration').value = dur;
                selectedOptionIds.length = 0;
                $('ysp-edit-options').innerHTML = optionPicker(this.value);
            });
        });
    }

    function debounce(fn, wait) {
        let timer = null;
        return function() {
            clearTimeout(timer);
            timer = setTimeout(fn, wait);
        };
    }

    function searchClients() {
        const term = $('ysp-customer-search').value.trim();
        if (term.length < 2) {
            $('ysp-client-results').innerHTML = '';
            return;
        }
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_search_clients',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            term,
        }, function(resp) {
            const clients = resp.success ? (resp.data.clients || []) : [];
            $('ysp-client-results').innerHTML = clients.map(c => `
                <button type="button" class="ysp-client-pick" data-name="${escapeHTML(c.customer_name)}" data-phone="${escapeHTML(c.customer_phone)}" data-email="${escapeHTML(c.customer_email)}">
                    <strong>${escapeHTML(c.customer_name || c.customer_phone || c.customer_email)}</strong>
                    <span>${escapeHTML(c.customer_phone || '')} ${escapeHTML(c.customer_email || '')}</span>
                </button>`).join('');
            document.querySelectorAll('.ysp-client-pick').forEach(btn => {
                btn.addEventListener('click', function() {
                    $('ysp-edit-name').value = this.dataset.name || '';
                    $('ysp-edit-phone').value = this.dataset.phone || '';
                    $('ysp-edit-email').value = this.dataset.email || '';
                    $('ysp-client-results').innerHTML = '';
                });
            });
        });
    }

    function saveBooking(bookingId) {
        const btn = $('ysp-save-booking');
        btn.disabled = true;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_save_booking',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            booking_id: bookingId || 0,
            customer_name: $('ysp-edit-name').value.trim(),
            customer_phone: $('ysp-edit-phone').value.trim(),
            customer_email: $('ysp-edit-email').value.trim(),
            service_id: $('ysp-edit-service').value,
            booking_staff_id: $('ysp-edit-staff').value,
            date: $('ysp-edit-date').value,
            time: $('ysp-edit-time').value,
            duration_minutes: $('ysp-edit-duration').value,
            option_ids: JSON.stringify(Array.from(document.querySelectorAll('#ysp-edit-options input:checked')).map(el => parseInt(el.value, 10))),
            notes: $('ysp-edit-notes').value,
        }, function(resp) {
            btn.disabled = false;
            if (resp.success) {
                toast(resp.data.message || 'Saved');
                closeEditor();
                closeDetail();
                loadSchedule(true);
            } else {
                toast(resp.data && resp.data.message ? resp.data.message : 'Could not save');
            }
        }).fail(function() {
            btn.disabled = false;
            toast('Network error');
        });
    }

    function cancelBooking(bookingId) {
        if (!state.permissions.cancel_booking || !window.confirm('Cancel this appointment? If this is a group appointment, related guest appointments will be cancelled too.')) return;
        jQuery.post(yspData.ajaxUrl, {
            action: 'yatoon_staff_cancel_booking',
            nonce: yspData.nonce,
            staff_id: state.staffId,
            booking_id: bookingId,
        }, function(resp) {
            if (resp.success) {
                toast(resp.data.message || 'Cancelled');
                closeDetail();
                loadSchedule(true);
            } else {
                toast(resp.data && resp.data.message ? resp.data.message : 'Could not cancel');
            }
        });
    }
    /* Update status */
    function updateStatus(bookingIds, newStatus) {
        const ids = Array.isArray(bookingIds) ? bookingIds : [bookingIds];
        const btns = $('ysp-sheet-body').querySelectorAll('[data-action]');
        btns.forEach(b => b.disabled = true);

        let remaining = ids.length;
        let anyFailed = false;

        ids.forEach(bookingId => {
            jQuery.post(yspData.ajaxUrl, {
                action:     'yatoon_staff_update_booking_status',
                nonce:      yspData.nonce,
                staff_id:   state.staffId,
                booking_id: bookingId,
                status:     newStatus,
            }, function (resp) {
                remaining -= 1;
                if (resp.success) {
                    Object.keys(state.schedule).forEach(date => {
                        state.schedule[date].forEach(b => {
                            if (String(b.id) === String(bookingId)) b.status = newStatus;
                        });
                    });
                } else {
                    anyFailed = true;
                }
                if (remaining === 0) {
                    if (anyFailed) {
                        toast('Some services could not be updated.');
                        btns.forEach(b => b.disabled = false);
                    } else {
                        const shouldOpenFeeSheet = newStatus === 'no_show' && openNoShowFeeSheet(state.activeBooking || null);
                        if (!shouldOpenFeeSheet) {
                            toast('Status updated');
                            closeDetail();
                        }
                    }
                    renderSchedule();
                }
            }).fail(function () {
                remaining -= 1;
                anyFailed = true;
                if (remaining === 0) {
                    toast('Network error');
                    btns.forEach(b => b.disabled = false);
                }
            });
        });
    }

    /* Service Worker */
    function registerSW() {
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.getRegistrations().then(registrations => {
                registrations.forEach(registration => {
                    const worker = registration.active || registration.waiting || registration.installing;
                    if (registration.scope === window.location.origin + '/' && worker && worker.scriptURL.includes('yatoon-')) {
                        registration.unregister();
                    }
                });
            }).finally(() => {
                navigator.serviceWorker.register(yspData.swUrl, { scope: yspData.swScope || window.location.pathname })
                    .catch(() => {}); // silent fail, SW is optional
            });
        }
    }

    /* Utils */
    function escapeHTML(str) {
        return String(str || '').replace(/[&<>"']/g, m => ({
            '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
        })[m]);
    }

    /* Init */
    function init() {
        try {
            const savedMode = localStorage.getItem('ysp_calendar_mode');
            if (['day', 'week', 'list', 'month'].includes(savedMode)) {
                state.calendarMode = savedMode;
                state.hasSavedCalendarMode = true;
            }
        } catch (e) {}
        updateCalendarControls();
        // Numpad
        document.querySelectorAll('.ysp-numpad-btn[data-digit]').forEach(btn => {
            btn.addEventListener('click', () => handleDigit(btn.dataset.digit));
        });
        $('ysp-backspace').addEventListener('click', handleBackspace);

        // Logout
        $('ysp-logout-btn').addEventListener('click', logout);

        // Refresh
        $('ysp-refresh-btn').addEventListener('click', () => {
            loadSchedule(false);
            toast('Refreshing...', 1000);
        });
        if ($('ysp-add-booking-btn')) {
            $('ysp-add-booking-btn').addEventListener('click', () => openBookingEditor(null));
        }
        document.querySelectorAll('.ysp-bottom-nav-btn[data-view]').forEach(btn => {
            btn.addEventListener('click', () => switchPortalView(btn.dataset.view));
        });
        if ($('ysp-bottom-more-btn')) {
            $('ysp-bottom-more-btn').addEventListener('click', renderMoreMenu);
        }
        document.querySelectorAll('[data-calendar-mode]').forEach(btn => {
            btn.addEventListener('click', () => setCalendarMode(btn.dataset.calendarMode));
        });
        if ($('ysp-calendar-options-btn')) {
            $('ysp-calendar-options-btn').addEventListener('click', renderCalendarSettings);
        }
        if ($('ysp-bottom-refresh-btn')) {
            $('ysp-bottom-refresh-btn').addEventListener('click', () => {
                loadSchedule(false);
                toast('Refreshing...', 1000);
            });
        }
        if ($('ysp-bottom-logout-btn')) {
            $('ysp-bottom-logout-btn').addEventListener('click', logout);
        }

        // Overlay close (tap backdrop)
        $('ysp-detail-overlay').addEventListener('click', function (e) {
            if (e.target === this) closeDetail();
        });
        $('ysp-editor-overlay').addEventListener('click', function (e) {
            if (e.target === this) closeEditor();
        });
        document.addEventListener('click', function(e) {
            const closeTarget = e.target.closest('[data-ysp-close]');
            if (!closeTarget) return;
            e.preventDefault();
            e.stopPropagation();
            if (closeTarget.dataset.yspClose === 'editor') {
                closeEditor();
            } else {
                closeDetail();
            }
        }, true);

        // Session restore
        const savedId   = sessionStorage.getItem('ysp_staff_id');
        const savedName = sessionStorage.getItem('ysp_staff_name');
        const savedRole = sessionStorage.getItem('ysp_staff_role');
        const savedToken = sessionStorage.getItem('ysp_portal_token');
        if (savedId && savedName) {
            state.staffId   = savedId;
            state.staffName = savedName;
            state.staffRole = savedRole || 'staff';
            state.portalToken = savedToken || '';
            try {
                state.permissions = Object.assign({}, state.permissions, JSON.parse(sessionStorage.getItem('ysp_permissions') || '{}'));
            } catch (e) {}
            updatePermissionUI();
            applyDefaultCalendarModeForRole();
            enterPortal();
        }

        // PWA install prompt (Android)
        let deferredPrompt;
        window.addEventListener('beforeinstallprompt', e => {
            e.preventDefault();
            deferredPrompt = e;
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();
