/* modules/free/appointments/assets/js/admin.js */
(function (window, $) {
    'use strict';

    var modules = window.noesiaSwaAdminModules || (window.noesiaSwaAdminModules = {});
    var nsVars = window.noesia_admin_vars || {};
    var ajaxUrl = nsVars.ajax_url || '';
    var nonces = {
        request: nsVars.nonce || ''
    };

    function getShell() {
        return window.noesiaSwaAdminShell || null;
    }

    var state = {
        booted: false,
        bound: false,
        cache: [],
        loading: false,
        view: 'month',
        cursor: null,
        selectedDate: null,
        backendMissing: false
    };

    function escHtml(text) {
        return String(text == null ? '' : text)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    function hasUi() {
        return $('#noesia-swa-appt-calendar').length > 0 ||
            $('#noesia-swa-appt-modal').length > 0 ||
            $('#noesia-swa-dashboard-appointments-count').length > 0 ||
            $('#noesia-swa-briefing-appointments-count').length > 0 ||
            $('#noesia-swa-morning-briefing-appointments-count').length > 0 ||
            $('[data-noesia-swa-dashboard-appointments-count]').length > 0 ||
            $('[data-noesia-swa-briefing-appointments-count]').length > 0;
    }

    function apptTodayYmd() {
        var now = new Date();
        var y = now.getFullYear();
        var m = String(now.getMonth() + 1).padStart(2, '0');
        var d = String(now.getDate()).padStart(2, '0');

        return y + '-' + m + '-' + d;
    }

    function apptParseDate(ymd) {
        if (!/^\d{4}-\d{2}-\d{2}$/.test(String(ymd || ''))) {
            return null;
        }

        var parts = String(ymd).split('-');

        return new Date(
            parseInt(parts[0], 10),
            parseInt(parts[1], 10) - 1,
            parseInt(parts[2], 10)
        );
    }

    function apptDateKey(dateObj) {
        return [
            String(dateObj.getFullYear()),
            String(dateObj.getMonth() + 1).padStart(2, '0'),
            String(dateObj.getDate()).padStart(2, '0')
        ].join('-');
    }

    function apptEnsureState() {
        var today = apptTodayYmd();
        var dt;

        if (!state.selectedDate) {
            state.selectedDate = today;
        }

        if (!state.cursor) {
            dt = apptParseDate(today);
            state.cursor = new Date(dt.getFullYear(), dt.getMonth(), 1);
        }
    }

    function apptFormatLong(ymd) {
        var dt = apptParseDate(ymd);

        if (!dt) {
            return '—';
        }

        return dt.toLocaleDateString([], {
            weekday: 'long',
            day: 'numeric',
            month: 'long',
            year: 'numeric'
        });
    }

    function apptMonthLabel() {
        apptEnsureState();

        return state.cursor.toLocaleDateString([], {
            month: 'long',
            year: 'numeric'
        });
    }

    function apptItemsForDate(ymd) {
        return state.cache
            .filter(function (item) {
                return String(item.date || '') === String(ymd || '');
            })
            .sort(function (a, b) {
                return String(a.start || '').localeCompare(String(b.start || ''));
            });
    }

    function apptSetMsg(text, isError) {
        var $msg = $('#noesia-swa-appt-msg');

        if (!$msg.length) {
            return;
        }

        $msg
            .text(String(text || ''))
            .toggleClass('is-error', !!isError);
    }

    function apptRenderDashboardSummary() {
        var today = apptTodayYmd();
        var todayCount = 0;
        var upcomingCount = 0;
        var $wrapper = $('#noesia-swa-wrapper');

        state.cache.forEach(function (item) {
            if (!item || item.status === 'cancelled') {
                return;
            }

            if (item.date === today) {
                todayCount += 1;
            }

            if (String(item.date || '') >= today && item.status !== 'completed') {
                upcomingCount += 1;
            }
        });

        if ($wrapper.length) {
            $wrapper.attr('data-noesia-swa-appointments-today-count', String(todayCount));
            $wrapper.attr('data-noesia-swa-appointments-upcoming-count', String(upcomingCount));
            $wrapper.attr('data-noesia-swa-active-appointments-count', String(todayCount));
        }

        var shell = getShell();

        if (shell && typeof shell.setActiveAppointmentsCount === 'function') {
            shell.setActiveAppointmentsCount(todayCount, true);
        }

        [
            '#noesia-swa-briefing-appointments-count',
            '#noesia-swa-morning-briefing-appointments-count',
            '#noesia-swa-dashboard-appointments-count',
            '[data-noesia-swa-briefing-appointments-count]',
            '[data-noesia-swa-dashboard-appointments-count]'
        ].forEach(function (selector) {
            var $el = $(selector);
            if ($el.length) {
                $el.text(String(todayCount));
            }
        });

        [
            '#noesia-swa-briefing-appointments-label',
            '#noesia-swa-morning-briefing-appointments-label',
            '#noesia-swa-dashboard-appointments-label',
            '[data-noesia-swa-briefing-appointments-label]',
            '[data-noesia-swa-dashboard-appointments-label]'
        ].forEach(function (selector) {
            var $el = $(selector);
            if ($el.length) {
                $el.text(todayCount === 1 ? 'Appointment today' : 'Appointments today');
            }
        });
    }

    function apptOpenModal(item) {
        var $modal = $('#noesia-swa-appt-modal');

        if (!$modal.length) {
            return;
        }

        if (item && typeof item === 'object') {
            $('#noesia-swa-appt-modal-title').text('Edit Appointment');
            $('#noesia-swa-appt-id').val(item.id || '');
            $('#noesia-swa-appt-date').val(item.date || '');
            $('#noesia-swa-appt-status').val(item.status || 'scheduled');
            $('#noesia-swa-appt-start').val(item.start || '');
            $('#noesia-swa-appt-end').val(item.end || '');
            $('#noesia-swa-appt-guest-name').val(item.guestName || '');
            $('#noesia-swa-appt-guest-phone').val(item.guestPhone || '');
            $('#noesia-swa-appt-guest-email').val(item.guestEmail || '');
            $('#noesia-swa-appt-reason').val(item.reason || '');
            $('#noesia-swa-appt-party-size').val(item.partySize || '');
            $('#noesia-swa-appt-location').val(item.location || '');
            $('#noesia-swa-appt-notes').val(item.notes || '');
            $('#noesia-swa-appt-delete').removeClass('noesia-swa-is-hidden');
        } else {
            resetApptForm();
            $('#noesia-swa-appt-modal-title').text('New Appointment');
            $('#noesia-swa-appt-delete').addClass('noesia-swa-is-hidden');
        }

        apptSetMsg('', false);
        $modal.addClass('is-open').attr('aria-hidden', 'false');
        $('body').addClass('noesia-swa-modal-open');
        $('#noesia-swa-appt-guest-name').trigger('focus');
    }

    function apptCloseModal() {
        $('#noesia-swa-appt-modal').removeClass('is-open').attr('aria-hidden', 'true');
        $('body').removeClass('noesia-swa-modal-open');
        apptSetMsg('', false);
    }

    function resetApptForm() {
        $('#noesia-swa-appt-id').val('');
        $('#noesia-swa-appt-date').val(state.selectedDate || apptTodayYmd());
        $('#noesia-swa-appt-status').val('scheduled');
        $('#noesia-swa-appt-start').val('');
        $('#noesia-swa-appt-end').val('');
        $('#noesia-swa-appt-guest-name').val('');
        $('#noesia-swa-appt-guest-phone').val('');
        $('#noesia-swa-appt-guest-email').val('');
        $('#noesia-swa-appt-reason').val('');
        $('#noesia-swa-appt-party-size').val('');
        $('#noesia-swa-appt-location').val('');
        $('#noesia-swa-appt-notes').val('');
        $('#noesia-swa-appt-delete').addClass('noesia-swa-is-hidden');
        apptSetMsg('', false);
    }

    function collectApptForm() {
        return {
            post_id: String($('#noesia-swa-appt-id').val() || '').trim(),
            date: String($('#noesia-swa-appt-date').val() || '').trim(),
            status: String($('#noesia-swa-appt-status').val() || 'scheduled').trim(),
            start: String($('#noesia-swa-appt-start').val() || '').trim(),
            end: String($('#noesia-swa-appt-end').val() || '').trim(),
            guest_name: String($('#noesia-swa-appt-guest-name').val() || '').trim(),
            guest_phone: String($('#noesia-swa-appt-guest-phone').val() || '').trim(),
            guest_email: String($('#noesia-swa-appt-guest-email').val() || '').trim(),
            reason: String($('#noesia-swa-appt-reason').val() || '').trim(),
            party_size: String($('#noesia-swa-appt-party-size').val() || '').trim(),
            location: String($('#noesia-swa-appt-location').val() || '').trim(),
            notes: String($('#noesia-swa-appt-notes').val() || '').trim()
        };
    }

    function validateApptForm(payload) {
        payload = payload || collectApptForm();

        if (!payload.date) {
            return { valid: false, message: 'Date is required.', field: '#noesia-swa-appt-date' };
        }

        if (!/^\d{4}-\d{2}-\d{2}$/.test(payload.date)) {
            return { valid: false, message: 'Date format is invalid.', field: '#noesia-swa-appt-date' };
        }

        if (!payload.start) {
            return { valid: false, message: 'Start Time is required.', field: '#noesia-swa-appt-start' };
        }

        if (!/^\d{2}:\d{2}$/.test(payload.start)) {
            return { valid: false, message: 'Start Time format is invalid.', field: '#noesia-swa-appt-start' };
        }

        if (payload.end && !/^\d{2}:\d{2}$/.test(payload.end)) {
            return { valid: false, message: 'End Time format is invalid.', field: '#noesia-swa-appt-end' };
        }

        if (payload.end && payload.end <= payload.start) {
            return { valid: false, message: 'End Time must be after Start Time.', field: '#noesia-swa-appt-end' };
        }

        if (!payload.guest_name) {
            return { valid: false, message: 'Guest Name is required.', field: '#noesia-swa-appt-guest-name' };
        }

        return { valid: true };
    }

    function apptNormalizeItem(item) {
        if (!item || typeof item !== 'object') {
            return null;
        }

        return {
            id: String(item.id || ''),
            date: String(item.date || '').trim(),
            start: String(item.start || '').trim(),
            end: String(item.end || '').trim(),
            guestName: String(item.guestName || '').trim(),
            guestPhone: String(item.guestPhone || '').trim(),
            guestEmail: String(item.guestEmail || '').trim(),
            reason: String(item.reason || '').trim(),
            partySize: String(item.partySize || '').trim(),
            location: String(item.location || '').trim(),
            notes: String(item.notes || '').trim(),
            status: String(item.status || 'scheduled').trim()
        };
    }

    function apptFindById(id) {
        var found = null;

        state.cache.some(function (item) {
            if (item.id === id) {
                found = item;
                return true;
            }
            return false;
        });

        return found;
    }

    function apptBadgeClass(item) {
        if (item.status === 'completed') {
            return 'is-completed';
        }
        if (item.status === 'urgent' || item.status === 'no_show') {
            return 'is-flagged';
        }
        if (item.date === apptTodayYmd()) {
            return 'is-today';
        }
        return 'is-scheduled';
    }

    function apptBadgeText(item) {
        if (item.status === 'completed') {
            return 'Completed';
        }
        if (item.status === 'cancelled') {
            return 'Cancelled';
        }
        if (item.status === 'no_show') {
            return 'No-Show';
        }
        if (item.status === 'urgent') {
            return 'Urgent';
        }
        if (item.date === apptTodayYmd()) {
            return 'Today';
        }
        return 'Scheduled';
    }

    function apptRenderMonth() {
        var $cal = $('#noesia-swa-appt-calendar');
        var today;
        var selected;
        var month;
        var first;
        var offset;
        var start;
        var html = '';
        var i;

        if (!$cal.length) {
            return;
        }

        apptEnsureState();
        $('#noesia-swa-appt-month-label').text(apptMonthLabel());

        today = apptTodayYmd();
        selected = state.selectedDate || today;
        month = state.cursor.getMonth();
        first = new Date(state.cursor.getFullYear(), state.cursor.getMonth(), 1);
        offset = (first.getDay() + 6) % 7;
        start = new Date(first);
        start.setDate(first.getDate() - offset);

        for (i = 0; i < 42; i += 1) {
            var dt = new Date(start);
            var ymd;
            var items;
            var scheduledCount;
            var flaggedCount;
            var cls = 'noesia-swa-appt-day';

            dt.setDate(start.getDate() + i);
            ymd = apptDateKey(dt);
            items = apptItemsForDate(ymd);
            scheduledCount = items.filter(function (item) {
                return item.status === 'scheduled' || item.status === 'urgent';
            }).length;
            flaggedCount = items.filter(function (item) {
                return item.status === 'no_show';
            }).length;

            if (dt.getMonth() !== month) {
                cls += ' is-other-month';
            }
            if (ymd === today) {
                cls += ' is-today';
            }
            if (ymd === selected) {
                cls += ' is-selected';
            }
            if (items.length) {
                cls += ' has-items';
            }

            html += '<button type="button" class="' + cls + '" data-noesia-swa-appt-date="' + escHtml(ymd) + '">';
            html += '<div class="noesia-swa-appt-day__top">';
            html += '<span class="noesia-swa-appt-day__num">' + escHtml(String(dt.getDate())) + '</span>';
            html += '<span class="noesia-swa-appt-day__counters">';

            if (scheduledCount > 0) {
                html += '<span class="noesia-swa-appt-counter is-scheduled">' + escHtml(String(scheduledCount)) + '</span>';
            }
            if (flaggedCount > 0) {
                html += '<span class="noesia-swa-appt-counter is-flagged">' + escHtml(String(flaggedCount)) + '</span>';
            }

            html += '</span></div>';

            items.slice(0, 2).forEach(function (item) {
                var guest = String(item.guestName || item.reason || 'Appointment');
                if (guest.length > 12) {
                    guest = guest.slice(0, 11) + '…';
                }
                html += '<div class="noesia-swa-appt-micro-line">' + escHtml((item.start || '--:--') + ' ' + guest) + '</div>';
            });

            if (items.length > 2) {
                html += '<div class="noesia-swa-appt-micro-more">+' + escHtml(String(items.length - 2)) + '</div>';
            }

            html += '</button>';
        }

        $cal.html(html);
    }

    function apptRenderWeek() {
        var $grid = $('#noesia-swa-appt-week-grid');
        var selected;
        var offset;
        var start;
        var html = '';
        var i;

        if (!$grid.length) {
            return;
        }

        selected = apptParseDate(state.selectedDate || apptTodayYmd());
        offset = (selected.getDay() + 6) % 7;
        start = new Date(selected);
        start.setDate(selected.getDate() - offset);

        for (i = 0; i < 7; i += 1) {
            var dt = new Date(start);
            var ymd;
            var items;

            dt.setDate(start.getDate() + i);
            ymd = apptDateKey(dt);
            items = apptItemsForDate(ymd);

            html += '<div class="noesia-swa-appt-week-day">';
            html += '<button type="button" class="noesia-swa-appt-week-day__head" data-noesia-swa-appt-date="' + escHtml(ymd) + '">';
            html += '<strong>' + escHtml(dt.toLocaleDateString('en-GB', { weekday: 'short' })) + '</strong>';
            html += '<span>' + escHtml(dt.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })) + '</span>';
            html += '</button>';
            html += '<div class="noesia-swa-appt-week-day__list">';

            if (!items.length) {
                html += '<div class="noesia-swa-appt-week-empty">—</div>';
            } else {
                items.slice(0, 6).forEach(function (item) {
                    html += '<button type="button" class="noesia-swa-appt-week-item" data-noesia-swa-appt-edit="' + escHtml(item.id || '') + '">';
                    html += '<span class="noesia-swa-appt-week-item__time">' + escHtml(item.start || '--:--') + '</span>';
                    html += '<span class="noesia-swa-appt-week-item__name">' + escHtml(item.guestName || item.reason || 'Appointment') + '</span>';
                    html += '</button>';
                });
            }

            html += '</div></div>';
        }

        $grid.html(html);
    }

    function apptRenderAgenda() {
        var $list = $('#noesia-swa-appt-agenda-list');
        var today = apptTodayYmd();
        var items;
        var html = '';

        if (!$list.length) {
            return;
        }

        items = state.cache
            .filter(function (item) {
                return String(item.date || '') >= today;
            })
            .sort(function (a, b) {
                var byDate = String(a.date || '').localeCompare(String(b.date || ''));
                if (byDate !== 0) {
                    return byDate;
                }
                return String(a.start || '').localeCompare(String(b.start || ''));
            });

        if (!items.length) {
            $list.html('<div class="noesia-swa-appt-agenda-empty">No upcoming appointments.</div>');
            return;
        }

        items.slice(0, 30).forEach(function (item) {
            html += '<button type="button" class="noesia-swa-appt-agenda-item" data-noesia-swa-appt-edit="' + escHtml(item.id || '') + '">';
            html += '<div class="noesia-swa-appt-agenda-item__date">' + escHtml(apptFormatLong(item.date)) + '</div>';
            html += '<div class="noesia-swa-appt-agenda-item__main">';
            html += '<strong>' + escHtml((item.start || '--:--') + ' · ' + (item.guestName || item.reason || 'Appointment')) + '</strong>';
            html += '<span>' + escHtml(item.location || item.reason || '') + '</span>';
            html += '</div>';
            html += '</button>';
        });

        $list.html(html);
    }

    function apptRenderDayPanel() {
        var ymd = state.selectedDate || apptTodayYmd();
        var $list = $('#noesia-swa-appt-day-list');
        var items;
        var html = '';

        if (!$list.length) {
            return;
        }

        items = apptItemsForDate(ymd);
        $('#noesia-swa-appt-day-title').text(apptFormatLong(ymd));
        $('#noesia-swa-appt-day-meta').text(items.length === 1 ? '1 appointment' : (items.length + ' appointments'));

        if (!items.length) {
            $list.html('<div class="noesia-swa-appt-day-empty">No appointments for this day.</div>');
            return;
        }

        items.forEach(function (item) {
            html += '<article class="noesia-swa-appt-item">';
            html += '<div class="noesia-swa-appt-item__top">';
            html += '<div class="noesia-swa-appt-item__title">' + escHtml(item.guestName || item.reason || 'Appointment') + '</div>';
            html += '<span class="noesia-swa-appt-badge ' + escHtml(apptBadgeClass(item)) + '">' + escHtml(apptBadgeText(item)) + '</span>';
            html += '</div>';
            html += '<div class="noesia-swa-appt-item__meta">';
            html += '<span><strong>Time:</strong> ' + escHtml(item.start || '--:--') + (item.end ? ' – ' + escHtml(item.end) : '') + '</span>';

            if (item.reason) {
                html += '<span><strong>Purpose:</strong> ' + escHtml(item.reason) + '</span>';
            }
            if (item.location) {
                html += '<span><strong>Area:</strong> ' + escHtml(item.location) + '</span>';
            }
            if (item.partySize) {
                html += '<span><strong>Guests:</strong> ' + escHtml(item.partySize) + '</span>';
            }

            html += '</div>';

            if (item.notes) {
                html += '<div class="noesia-swa-appt-item__notes">' + escHtml(item.notes) + '</div>';
            }

            html += '<div class="noesia-swa-appt-item__actions">';
            html += '<button type="button" class="button" data-noesia-swa-appt-edit="' + escHtml(item.id || '') + '">Edit</button>';
            html += '</div>';
            html += '</article>';
        });

        $list.html(html);
    }

    function apptRenderStats() {
        var today = apptTodayYmd();
        var stats = {
            today: 0,
            upcoming: 0,
            scheduled: 0,
            completed: 0
        };

        state.cache.forEach(function (item) {
            if (item.date === today) {
                stats.today += 1;
            }
            if (String(item.date || '') >= today && item.status !== 'completed' && item.status !== 'cancelled') {
                stats.upcoming += 1;
            }
            if (item.status === 'scheduled' || item.status === 'urgent' || item.status === 'no_show') {
                stats.scheduled += 1;
            }
            if (item.status === 'completed') {
                stats.completed += 1;
            }
        });

        $('#noesia-swa-appt-kpi-today').text(String(stats.today));
        $('#noesia-swa-appt-kpi-upcoming').text(String(stats.upcoming));
        $('#noesia-swa-appt-kpi-scheduled').text(String(stats.scheduled));
        $('#noesia-swa-appt-kpi-completed').text(String(stats.completed));
    }

    function renderAppointmentsUi() {
        apptRenderMonth();
        apptRenderWeek();
        apptRenderAgenda();
        apptRenderDayPanel();
        apptRenderStats();
        apptRenderDashboardSummary();
    }

    function apptSetView(view) {
        var allowed = ['month', 'week', 'agenda'];
        var next = allowed.indexOf(view) !== -1 ? view : 'month';

        state.view = next;

        $('[data-noesia-swa-appt-view]').removeClass('is-active').attr('aria-selected', 'false');
        $('[data-noesia-swa-appt-view="' + next + '"]').addClass('is-active').attr('aria-selected', 'true');
        $('[data-noesia-swa-appt-panel]').attr('hidden', true).removeClass('is-active');
        $('[data-noesia-swa-appt-panel="' + next + '"]').removeAttr('hidden').addClass('is-active');
    }

    function appointmentsAjax(action, data) {
        return $.ajax({
            url: ajaxUrl,
            method: 'POST',
            dataType: 'json',
            data: $.extend({}, data || {}, {
                action: action,
                nonce: nonces.request
            })
        });
    }

    function fetchAppointments() {
        if (!hasUi()) {
            return $.Deferred().resolve(false).promise();
        }

        if (!ajaxUrl || !nonces.request) {
            state.backendMissing = true;
            state.cache = [];
            renderAppointmentsUi();
            return $.Deferred().resolve(false).promise();
        }

        state.loading = true;

        return appointmentsAjax('noesia_swa_appointments_list', {})
            .done(function (resp) {
                if (resp && resp.success && resp.data && Array.isArray(resp.data.items)) {
                    state.backendMissing = false;
                    state.cache = resp.data.items
                        .map(apptNormalizeItem)
                        .filter(function (item) { return !!item; });
                } else {
                    state.backendMissing = true;
                    state.cache = [];
                }

                renderAppointmentsUi();
            })
            .fail(function () {
                state.backendMissing = true;
                state.cache = [];
                renderAppointmentsUi();
                apptSetMsg('Could not load appointments.', true);
            })
            .always(function () {
                state.loading = false;
            });
    }

    function submitApptForm() {
        var payload = collectApptForm();
        var validation = validateApptForm(payload);

        if (!validation.valid) {
            apptSetMsg(validation.message, true);
            if (validation.field) {
                $(validation.field).trigger('focus');
            }
            return $.Deferred().reject(validation).promise();
        }

        if (!ajaxUrl || !nonces.request) {
            apptSetMsg('Appointments backend is not ready yet.', true);
            return $.Deferred().reject().promise();
        }

        apptSetMsg('Saving appointment…', false);

        return appointmentsAjax('noesia_swa_appointments_save', payload)
            .done(function (resp) {
                if (!resp || !resp.success) {
                    apptSetMsg((resp && resp.data && resp.data.message) ? String(resp.data.message) : 'Could not save appointment.', true);
                    return;
                }

                apptCloseModal();
                fetchAppointments();
            })
            .fail(function () {
                apptSetMsg('Could not save appointment.', true);
            });
    }

    function deleteApptCurrent() {
        var postId = String($('#noesia-swa-appt-id').val() || '').trim();

        if (!postId) {
            return $.Deferred().resolve(false).promise();
        }

        apptSetMsg('Deleting appointment…', false);

        return appointmentsAjax('noesia_swa_appointments_delete', {
            post_id: postId
        }).done(function (resp) {
            if (!resp || !resp.success) {
                apptSetMsg((resp && resp.data && resp.data.message) ? String(resp.data.message) : 'Could not delete appointment.', true);
                return;
            }

            apptCloseModal();
            resetApptForm();
            fetchAppointments();
        }).fail(function () {
            apptSetMsg('Could not delete appointment.', true);
        });
    }

    function toggleApptStatus(id) {
        id = String(id || '').trim();

        if (!id) {
            return $.Deferred().resolve(false).promise();
        }

        return appointmentsAjax('noesia_swa_appointments_toggle', {
            post_id: id
        }).done(function (resp) {
            if (!resp || !resp.success) {
                apptSetMsg((resp && resp.data && resp.data.message) ? String(resp.data.message) : 'Could not update appointment.', true);
                return;
            }

            fetchAppointments();
        }).fail(function () {
            apptSetMsg('Could not update appointment.', true);
        });
    }

    function bindEvents() {
        if (state.bound) {
            return true;
        }

        state.bound = true;

        $(document)
            .off('.noesiaSwaAppointments')
            .on('click.noesiaSwaAppointments', '#noesia-swa-appt-prev', function (e) {
                e.preventDefault();

                if (state.view === 'month') {
                    state.cursor.setMonth(state.cursor.getMonth() - 1);
                } else {
                    var prev = apptParseDate(state.selectedDate || apptTodayYmd());
                    prev.setDate(prev.getDate() - 7);
                    state.selectedDate = apptDateKey(prev);
                    state.cursor = new Date(prev.getFullYear(), prev.getMonth(), 1);
                }

                renderAppointmentsUi();
            })
            .on('click.noesiaSwaAppointments', '#noesia-swa-appt-next', function (e) {
                e.preventDefault();

                if (state.view === 'month') {
                    state.cursor.setMonth(state.cursor.getMonth() + 1);
                } else {
                    var next = apptParseDate(state.selectedDate || apptTodayYmd());
                    next.setDate(next.getDate() + 7);
                    state.selectedDate = apptDateKey(next);
                    state.cursor = new Date(next.getFullYear(), next.getMonth(), 1);
                }

                renderAppointmentsUi();
            })
            .on('click.noesiaSwaAppointments', '#noesia-swa-appt-today', function (e) {
                e.preventDefault();
                var today = apptParseDate(apptTodayYmd());
                state.selectedDate = apptDateKey(today);
                state.cursor = new Date(today.getFullYear(), today.getMonth(), 1);
                renderAppointmentsUi();
            })
            .on('click.noesiaSwaAppointments', '[data-noesia-swa-appt-view]', function (e) {
                e.preventDefault();
                apptSetView(String($(this).attr('data-noesia-swa-appt-view') || 'month'));
                renderAppointmentsUi();
            })
            .on('click.noesiaSwaAppointments', '#noesia-swa-appt-new', function (e) {
                e.preventDefault();
                apptOpenModal(null);
            })
            .on('click.noesiaSwaAppointments', '[data-noesia-swa-appt-date]', function (e) {
                e.preventDefault();
                state.selectedDate = String($(this).attr('data-noesia-swa-appt-date') || apptTodayYmd());
                renderAppointmentsUi();
            })
            .on('click.noesiaSwaAppointments', '[data-noesia-swa-appt-edit]', function (e) {
                e.preventDefault();
                var item = apptFindById(String($(this).attr('data-noesia-swa-appt-edit') || ''));
                if (item) {
                    apptOpenModal(item);
                }
            })
            .on('click.noesiaSwaAppointments', '[data-noesia-swa-appt-modal-close]', function (e) {
                e.preventDefault();
                apptCloseModal();
            })
            .on('click.noesiaSwaAppointments', '#noesia-swa-appt-save', function (e) {
                e.preventDefault();
                e.stopPropagation();
                submitApptForm();
            })
            .on('click.noesiaSwaAppointments', '#noesia-swa-appt-delete', function (e) {
                e.preventDefault();
                e.stopPropagation();
                deleteApptCurrent();
            });

        $(document).on('keydown.noesiaSwaAppointments', function (e) {
            if (e.key === 'Escape' && $('#noesia-swa-appt-modal').hasClass('is-open')) {
                apptCloseModal();
            }
        });

        return true;
    }

    function boot() {
        if (state.booted) {
            return true;
        }

        if (!hasUi()) {
            return false;
        }

        apptEnsureState();
        apptSetView(state.view || 'month');
        bindEvents();
        state.booted = true;
        return true;
    }

    function init() {
        if (!boot()) {
            return false;
        }

        fetchAppointments();
        return true;
    }

    function onSectionOpen(target) {
        var current = String(target || 'appointments');
        
        if (current !== 'appointments' && current !== 'dashboard') {
            return false;
        }

        return init();
    }

    function onShow() {
        return onSectionOpen('appointments');
    }

    function show() {
        return onSectionOpen('appointments');
    }

    modules.appointments = {
        boot: boot,
        init: init,
        onSectionOpen: onSectionOpen,
        onShow: onShow,
        show: show,
        fetch: fetchAppointments,
        resetApptForm: resetApptForm,
        validateApptForm: validateApptForm,
        submitApptForm: submitApptForm,
        deleteApptCurrent: deleteApptCurrent,
        toggleApptStatus: toggleApptStatus,
        getCache: function () {
            return state.cache.slice();
        }
    };

    $(init);
})(window, jQuery);