/* ----------------------------------------------------------------------------
 * Easy!Appointments - Online Appointment Scheduler
 *
 * @package     EasyAppointments
 * @author      A.Tselegidis <alextselegidis@gmail.com>
 * @copyright   Copyright (c) Alex Tselegidis
 * @license     https://opensource.org/licenses/GPL-3.0 - GPLv3
 * @link        https://easyappointments.org
 * @since       v1.5.0
 * ---------------------------------------------------------------------------- */

/**
 * Default calendar view utility.
 *
 * This module implements the functionality of the default calendar view.
 *
 * Old Name: BackendCalendarDefaultView
 */
App.Utils.CalendarDefaultView = (function () {
    const $calendarPage = $('#calendar-page');
    const $reloadAppointments = $('#reload-appointments');
    const $calendar = $('#calendar');
    const $selectFilterItem = $('#select-filter-item');
    const $appointmentsModal = $('#appointments-modal');
    const $unavailabilitiesModal = $('#unavailabilities-modal');
    const $header = $('#header');
    const $footer = $('#footer');
    const $notification = $('#notification');
    const $calendarToolbar = $('#calendar-toolbar');
    const FILTER_TYPE_ALL = 'all';
    const FILTER_TYPE_PROVIDER = 'provider';
    const FILTER_TYPE_SERVICE = 'service';
    const moment = window.moment;

    let $popoverTarget;
    let fullCalendar = null;
    let lastFocusedEventData; // Contains event data for later use.

    /**
     * Add the utility event listeners.
     */
    function addEventListeners() {
        /**
         * Event: Reload Button "Click"
         *
         * When the user clicks the reload button, the calendar items need to be refreshed.
         */
        $reloadAppointments.on('click', () => {
            const calendarView = fullCalendar.view;

            if ($popoverTarget) {
                $popoverTarget.popover('dispose');
            }

            refreshCalendarAppointments(
                $calendar,
                $selectFilterItem.val(),
                $selectFilterItem.find('option:selected').attr('type'),
                calendarView.currentStart,
                calendarView.currentEnd
            );
        });

        /**
         * Event: Popover Close Button "Click"
         *
         * Hides the open popover element.
         */
        $calendarPage.on('click', '.close-popover', () => {
            if ($popoverTarget) {
                $popoverTarget.popover('dispose');
            }
        });

        /**
         * Event: Popover Edit Button "Click"
         *
         * Enables the edit dialog of the selected calendar event.
         *
         * @param {jQuery.Event} event
         */
        $calendarPage.on('click', '.edit-popover', (event) => {
            if ($popoverTarget) {
                $popoverTarget.popover('dispose');
            }

            let startMoment;
            let endMoment;

            if (lastFocusedEventData.extendedProps.data.workingPlanException) {
                const date = lastFocusedEventData.extendedProps.data.date;
                const workingPlanException = lastFocusedEventData.extendedProps.data.workingPlanException;
                const provider = lastFocusedEventData.extendedProps.data.provider;

                App.Components.WorkingPlanExceptionsModal.edit(date, workingPlanException).done(
                    (date, workingPlanException) => {
                        const successCallback = () => {
                            App.Layouts.Backend.displayNotification(lang('working_plan_exception_saved'));

                            const workingPlanExceptions = JSON.parse(provider.settings.working_plan_exceptions) || {};

                            workingPlanExceptions[date] = workingPlanException;

                            for (const index in vars('available_providers')) {
                                const availableProvider = vars('available_providers')[index];

                                if (Number(availableProvider.id) === Number(provider.id)) {
                                    availableProvider.settings.working_plan_exceptions =
                                        JSON.stringify(workingPlanExceptions);
                                    break;
                                }
                            }

                            $reloadAppointments.trigger('click'); // Update the calendar.
                        };

                        App.Http.Calendar.saveWorkingPlanException(
                            date,
                            workingPlanException,
                            provider.id,
                            successCallback,
                            null
                        );
                    }
                );
            } else if (!lastFocusedEventData.extendedProps.data.is_unavailability) {
                const appointment = lastFocusedEventData.extendedProps.data;

                App.Components.AppointmentsModal.resetModal();

                // Apply appointment data and show modal dialog.
                $appointmentsModal.find('.modal-header h3').text(lang('edit_appointment_title'));
                $appointmentsModal.find('#appointment-id').val(appointment.id);
                $appointmentsModal.find('#select-service').val(appointment.id_services).trigger('change');
                $appointmentsModal.find('#select-provider').val(appointment.id_users_provider);

                // Set the start and end datetime of the appointment.
                startMoment = moment(appointment.start_datetime);
                $appointmentsModal.find('#start-datetime').datetimepicker('setDate', startMoment.toDate());

                endMoment = moment(appointment.end_datetime);
                $appointmentsModal.find('#end-datetime').datetimepicker('setDate', endMoment.toDate());

                const customer = appointment.customer;
                $appointmentsModal.find('#customer-id').val(appointment.id_users_customer);
                $appointmentsModal.find('#first-name').val(customer.first_name);
                $appointmentsModal.find('#last-name').val(customer.last_name);
                $appointmentsModal.find('#email').val(customer.email);
                $appointmentsModal.find('#phone-number').val(customer.phone_number);
                $appointmentsModal.find('#address').val(customer.address);
                $appointmentsModal.find('#city').val(customer.city);
                $appointmentsModal.find('#zip-code').val(customer.zip_code);
                $appointmentsModal.find('#language').val(customer.language);
                $appointmentsModal.find('#timezone').val(customer.timezone);
                $appointmentsModal.find('#appointment-location').val(appointment.location);
                $appointmentsModal.find('#appointment-notes').val(appointment.notes);
                $appointmentsModal.find('#customer-notes').val(customer.notes);

                App.Components.ColorSelection.setColor(
                    $appointmentsModal.find('#appointment-color'),
                    appointment.color
                );

                $appointmentsModal.modal('show');
            } else {
                const unavailability = lastFocusedEventData.extendedProps.data;

                // Replace string date values with actual date objects.
                unavailability.start_datetime = moment(lastFocusedEventData.start).format('YYYY-MM-DD HH:mm:ss');
                startMoment = moment(unavailability.start_datetime);
                unavailability.end_datetime = moment(lastFocusedEventData.end).format('YYYY-MM-DD HH:mm:ss');
                endMoment = moment(unavailability.end_datetime);

                App.Components.UnavailabilitiesModal.resetModal();

                // Apply unavailability data to dialog.
                $unavailabilitiesModal.find('.modal-header h3').text(lang('edit_unavailability_title'));
                $unavailabilitiesModal.find('#unavailability-start').datetimepicker('setDate', startMoment.toDate());
                $unavailabilitiesModal.find('#unavailability-id').val(unavailability.id);
                $unavailabilitiesModal.find('#unavailability-provider').val(unavailability.id_users_provider);
                $unavailabilitiesModal.find('#unavailability-end').datetimepicker('setDate', endMoment.toDate());
                $unavailabilitiesModal.find('#unavailability-notes').val(unavailability.notes);
                $unavailabilitiesModal.modal('show');
            }
        });

        /**
         * Event: Popover Delete Button "Click"
         *
         * Displays a prompt on whether the user wants the appointment to be deleted. If he confirms the
         * deletion then an AJAX call is made to the server and deletes the appointment from the database.
         *
         * @param {jQuery.Event} event
         */
        $calendarPage.on('click', '.delete-popover', (event) => {
            if ($popoverTarget) {
                $popoverTarget.popover('dispose');
            }

            if (lastFocusedEventData.extendedProps.data.workingPlanException) {
                const providerId = $selectFilterItem.val();

                const provider = vars('available_providers').find(
                    (availableProvider) => Number(availableProvider.id) === Number(providerId)
                );

                if (!provider) {
                    throw new Error('Provider could not be found: ' + providerId);
                }

                const successCallback = () => {
                    App.Layouts.Backend.displayNotification(lang('working_plan_exception_deleted'));

                    const workingPlanExceptions = JSON.parse(provider.settings.working_plan_exceptions) || {};
                    delete workingPlanExceptions[date];

                    for (const index in vars('available_providers')) {
                        const availableProvider = vars('available_providers')[index];

                        if (Number(availableProvider.id) === Number(providerId)) {
                            availableProvider.settings.working_plan_exceptions = JSON.stringify(workingPlanExceptions);
                            break;
                        }
                    }

                    $reloadAppointments.trigger('click'); // Update the calendar.
                };

                const date = moment(lastFocusedEventData.start).format('YYYY-MM-DD');

                App.Http.Calendar.deleteWorkingPlanException(date, providerId, successCallback);
            } else if (!lastFocusedEventData.extendedProps.data.is_unavailability) {
                const buttons = [
                    {
                        text: lang('cancel'),
                        click: () => {
                            $('#message-box').dialog('close');
                        }
                    },
                    {
                        text: lang('delete'),
                        click: () => {
                            const appointmentId = lastFocusedEventData.extendedProps.data.id;

                            const deleteReason = $('#delete-reason').val();

                            App.Http.Calendar.deleteAppointment(appointmentId, deleteReason).done(() => {
                                $('#message-box').dialog('close');

                                // Refresh calendar event items.
                                $reloadAppointments.trigger('click');
                            });
                        }
                    }
                ];

                App.Utils.Message.show(
                    lang('delete_appointment_title'),
                    lang('write_appointment_removal_reason'),
                    buttons
                );

                $('<textarea/>', {
                    'class': 'form-control w-100',
                    'id': 'delete-reason',
                    'rows': '3'
                }).appendTo('#message-box');
            } else {
                // Do not display confirmation prompt.

                const unavailabilityId = lastFocusedEventData.extendedProps.data.id;

                App.Http.Calendar.deleteUnavailability(unavailabilityId).done(() => {
                    $('#message-box').dialog('close');

                    // Refresh calendar event items.
                    $reloadAppointments.trigger('click');
                });
            }
        });

        /**
         * Event: Calendar Filter Item "Change"
         *
         * Load the appointments that correspond to the select filter item and display them on the calendar.
         */
        $selectFilterItem.on('change', () => {
            // If current value is service, then the sync buttons must be disabled.
            if ($selectFilterItem.find('option:selected').attr('type') === FILTER_TYPE_SERVICE) {
                $('#google-sync, #enable-sync, #insert-appointment, #insert-dropdown').prop('disabled', true);
                fullCalendar.setOption('selectable', false);
                fullCalendar.setOption('editable', false);
            } else {
                $('#google-sync, #enable-sync, #insert-appointment, #insert-dropdown').prop('disabled', false);
                fullCalendar.setOption('selectable', true);
                fullCalendar.setOption('editable', true);

                const providerId = $selectFilterItem.val();

                const provider = vars('available_providers').find(
                    (availableProvider) => Number(availableProvider.id) === Number(providerId)
                );

                if (provider && provider.timezone) {
                    $('.provider-timezone').text(vars('timezones')[provider.timezone]);
                }

                // If the user has already the sync enabled then apply the proper style changes.
                if ($selectFilterItem.find('option:selected').attr('google-sync') === 'true') {
                    $('#enable-sync').removeClass('btn-light').addClass('btn-secondary enabled');
                    $('#enable-sync span').text(lang('disable_sync'));
                    $('#google-sync').prop('disabled', false);
                } else {
                    $('#enable-sync').removeClass('btn-secondary enabled').addClass('btn-light');
                    $('#enable-sync span').text(lang('enable_sync'));
                    $('#google-sync').prop('disabled', true);
                }

                $('#insert-working-plan-exception').toggle(providerId !== App.Utils.CalendarDefaultView.FILTER_TYPE_ALL);
            }

            $reloadAppointments.trigger('click');
        });
    }

    /**
     * Get Calendar Component Height
     *
     * This method calculates the proper calendar height, in order to be displayed correctly, even when the
     * browser window is resizing.
     *
     * @return {Number} Returns the calendar element height in pixels.
     */
    function getCalendarHeight() {
        const result =
            window.innerHeight - $footer.outerHeight() - $header.outerHeight() - $calendarToolbar.outerHeight() - 60; // 60 for fine tuning
        return result > 500 ? result : 500; // Minimum height is 500px
    }

    /**
     * Get the event notes for the popup widget.
     *
     * @param {Event} event
     */
    function getEventNotes(event) {
        if (!event.extendedProps || !event.extendedProps.data || !event.extendedProps.data.notes) {
            return '-';
        }

        const notes = event.extendedProps.data.notes;

        return notes.length > 100 ? notes.substring(0, 100) + '...' : notes;
    }

    /**
     * Calendar Event "Click" Callback
     *
     * When the user clicks on an appointment object on the calendar, then a data preview popover is display
     * above the calendar item.
     *
     * @param {Object} info
     */
    function onEventClick(info) {
        const $target = $(info.el);

        if ($popoverTarget) {
            $popoverTarget.popover('dispose');
        }

        let $html;
        let displayEdit;
        let displayDelete;

        // Depending on where the user clicked the event (title or empty space) we
        // need to use different selectors to reach the parent element.

        if ($target.hasClass('fc-unavailability')) {
            displayEdit =
                $target.hasClass('fc-custom') && vars('privileges').appointments.edit === true ? '' : 'd-none';
            displayDelete =
                $target.hasClass('fc-custom') && vars('privileges').appointments.delete === true ? 'me-2' : 'd-none'; // Same value at the time.

            $html = $('<div/>', {
                'html': [
                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('start')
                    }),
                    $('<span/>', {
                        'text': App.Utils.Date.format(
                            moment(info.event.start).format('YYYY-MM-DD HH:mm:ss'),
                            vars('date_format'),
                            vars('time_format'),
                            true
                        )
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('end')
                    }),
                    $('<span/>', {
                        'text': App.Utils.Date.format(
                            moment(info.event.end).format('YYYY-MM-DD HH:mm:ss'),
                            vars('date_format'),
                            vars('time_format'),
                            true
                        )
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'text': lang('notes')
                    }),
                    $('<span/>', {
                        'text': getEventNotes(info.event)
                    }),
                    $('<br/>'),

                    $('<hr/>'),

                    $('<div/>', {
                        'class': 'd-flex justify-content-center',
                        'html': [
                            $('<button/>', {
                                'class': 'close-popover btn btn-outline-secondary me-2',
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-ban me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('close')
                                    })
                                ]
                            }),
                            $('<button/>', {
                                'class': 'delete-popover btn btn-outline-secondary ' + displayDelete,
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-trash-alt me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('delete')
                                    })
                                ]
                            }),
                            $('<button/>', {
                                'class': 'edit-popover btn btn-primary ' + displayEdit,
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-edit me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('edit')
                                    })
                                ]
                            })
                        ]
                    })
                ]
            });
        } else if ($target.hasClass('fc-working-plan-exception')) {
            displayDelete =
                $target.hasClass('fc-custom') && vars('privileges').appointments.delete === true ? 'me-2' : 'd-none';

            $html = $('<div/>', {
                'html': [
                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('provider')
                    }),
                    $('<span/>', {
                        'text': info.event.extendedProps.data
                            ? info.event.extendedProps.data.provider.first_name +
                            ' ' +
                            info.event.extendedProps.data.provider.last_name
                            : '-'
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('start')
                    }),
                    $('<span/>', {
                        'text': App.Utils.Date.format(
                            info.event.extendedProps.data.date +
                            ' ' +
                            info.event.extendedProps.data.workingPlanException.start,
                            vars('date_format'),
                            vars('time_format'),
                            true
                        )
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('end')
                    }),
                    $('<span/>', {
                        'text': App.Utils.Date.format(
                            info.event.extendedProps.data.date +
                            ' ' +
                            info.event.extendedProps.data.workingPlanException.end,
                            vars('date_format'),
                            vars('time_format'),
                            true
                        )
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('timezone')
                    }),
                    $('<span/>', {
                        'text': vars('timezones')[info.event.extendedProps.data.provider.timezone]
                    }),
                    $('<br/>'),

                    $('<hr/>'),

                    $('<div/>', {
                        'class': 'd-flex justify-content-between',
                        'html': [
                            $('<button/>', {
                                'class': 'close-popover btn btn-outline-secondary me-2',
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-ban me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('close')
                                    })
                                ]
                            }),
                            $('<button/>', {
                                'class': 'delete-popover btn btn-outline-secondary ' + displayDelete,
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-trash-alt me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('delete')
                                    })
                                ]
                            }),
                            $('<button/>', {
                                'class': 'edit-popover btn btn-primary',
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-edit me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('edit')
                                    })
                                ]
                            })
                        ]
                    })
                ]
            });
        } else {
            displayEdit = vars('privileges').appointments.edit === true ? '' : 'd-none';
            displayDelete = vars('privileges').appointments.delete === true ? 'me-2' : 'd-none';

            $html = $('<div/>', {
                'html': [
                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('start')
                    }),
                    $('<span/>', {
                        'text': App.Utils.Date.format(
                            moment(info.event.start).format('YYYY-MM-DD HH:mm:ss'),
                            vars('date_format'),
                            vars('time_format'),
                            true
                        )
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('end')
                    }),
                    $('<span/>', {
                        'text': App.Utils.Date.format(
                            moment(info.event.end).format('YYYY-MM-DD HH:mm:ss'),
                            vars('date_format'),
                            vars('time_format'),
                            true
                        )
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('timezone')
                    }),
                    $('<span/>', {
                        'text': vars('timezones')[info.event.extendedProps.data.provider.timezone]
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('service')
                    }),
                    $('<span/>', {
                        'text': info.event.extendedProps.data.service.name
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('provider')
                    }),
                    App.Utils.CalendarEventPopover.renderMapIcon(info.event.extendedProps.data.provider),
                    $('<span/>', {
                        'text':
                            info.event.extendedProps.data.provider.first_name +
                            ' ' +
                            info.event.extendedProps.data.provider.last_name
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('customer')
                    }),
                    App.Utils.CalendarEventPopover.renderMapIcon(info.event.extendedProps.data.customer),
                    $('<span/>', {
                        'class': 'd-inline-block ms-1',
                        'text':
                            info.event.extendedProps.data.customer.first_name +
                            ' ' +
                            info.event.extendedProps.data.customer.last_name
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('email')
                    }),
                    App.Utils.CalendarEventPopover.renderMailIcon(info.event.extendedProps.data.customer.email),
                    $('<span/>', {
                        'class': 'd-inline-block ms-1',
                        'text': info.event.extendedProps.data.customer.email
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'class': 'd-inline-block me-2',
                        'text': lang('phone')
                    }),
                    App.Utils.CalendarEventPopover.renderPhoneIcon(info.event.extendedProps.data.customer.phone_number),
                    $('<span/>', {
                        'class': 'd-inline-block ms-1',
                        'text': info.event.extendedProps.data.customer.phone_number
                    }),
                    $('<br/>'),

                    $('<strong/>', {
                        'text': lang('notes')
                    }),
                    $('<span/>', {
                        'text': getEventNotes(info.event)
                    }),
                    $('<br/>'),

                    $('<hr/>'),

                    $('<div/>', {
                        'class': 'd-flex justify-content-center',
                        'html': [
                            $('<button/>', {
                                'class': 'close-popover btn btn-outline-secondary me-2',
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-ban me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('close')
                                    })
                                ]
                            }),
                            $('<button/>', {
                                'class': 'delete-popover btn btn-outline-secondary ' + displayDelete,
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-trash-alt me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('delete')
                                    })
                                ]
                            }),
                            $('<button/>', {
                                'class': 'edit-popover btn btn-primary ' + displayEdit,
                                'html': [
                                    $('<i/>', {
                                        'class': 'fas fa-edit me-2'
                                    }),
                                    $('<span/>', {
                                        'text': lang('edit')
                                    })
                                ]
                            })
                        ]
                    })
                ]
            });
        }

        $target.popover({
            placement: 'top',
            title: info.event.title,
            content: $html,
            html: true,
            container: '#calendar',
            trigger: 'manual'
        });

        lastFocusedEventData = info.event;

        $target.popover('show');

        $popoverTarget = $target;

        // Fix popover position.
        const $popover = $calendarPage.find('.popover');

        if ($popover.length > 0 && $popover.position().top < 200) {
            $popover.css('top', '200px');
        }
    }

    /**
     * Calendar Event "Resize" Callback
     *
     * The user can change the duration of an event by resizing an appointment object on the calendar. This
     * change needs to be stored to the database too and this is done via an ajax call.
     *
     * @see updateAppointmentData()
     *
     * @param {Object} info
     */
    function onEventResize(info) {
        if (vars('privileges').appointments.edit === false) {
            info.revert();
            App.Layouts.Backend.displayNotification(lang('no_privileges_edit_appointments'));
            return;
        }

        let successCallback;

        if ($notification.is(':visible')) {
            $notification.hide('bind');
        }

        if (!info.event.extendedProps.data.is_unavailability) {
            // Prepare appointment data.
            info.event.extendedProps.data.end_datetime = moment(info.event.extendedProps.data.end_datetime)
                .add({days: info.endDelta.days, milliseconds: info.endDelta.milliseconds})
                .format('YYYY-MM-DD HH:mm:ss');

            const appointment = {...info.event.extendedProps.data};

            appointment.is_unavailability = Number(appointment.is_unavailability);

            // Must delete the following because only appointment data should be provided to the AJAX call.
            delete appointment.customer;
            delete appointment.provider;
            delete appointment.service;

            // Success callback
            successCallback = () => {
                // Display success notification to user.
                const undoFunction = () => {
                    appointment.end_datetime = info.event.extendedProps.data.end_datetime = moment(
                        appointment.end_datetime
                    )
                        .add({days: -info.endDelta.days, milliseconds: -info.endDelta.milliseconds})
                        .format('YYYY-MM-DD HH:mm:ss');

                    App.Http.Calendar.saveAppointment(appointment).done(() => {
                        $notification.hide('blind');
                    });

                    info.revert();
                };

                App.Layouts.Backend.displayNotification(lang('appointment_updated'), [
                    {
                        'label': lang('undo'),
                        'function': undoFunction
                    }
                ]);
                $footer.css('position', 'static'); // Footer position fix.

                // Update the event data for later use.
                info.event.setProp('data', info.event.extendedProps.data);
            };

            // Update appointment data.
            App.Http.Calendar.saveAppointment(appointment, null, successCallback);
        } else {
            // Update unavailability time period.
            const unavailability = {
                id: info.event.extendedProps.data.id,
                start_datetime: moment(info.event.start).format('YYYY-MM-DD HH:mm:ss'),
                end_datetime: moment(info.event.end).format('YYYY-MM-DD HH:mm:ss'),
                id_users_provider: info.event.extendedProps.data.id_users_provider
            };

            info.event.extendedProps.data.end_datetime = unavailability.end_datetime;

            // Define success callback function.
            successCallback = () => {
                // Display success notification to user.
                const undoFunction = () => {
                    unavailability.end_datetime = info.event.extendedProps.data.end_datetime = moment(
                        unavailability.end_datetime
                    )
                        .add({days: -info.delta.days, milliseconds: -info.delta.milliseconds})
                        .format('YYYY-MM-DD HH:mm:ss');

                    unavailability.is_unavailability = Number(unavailability.is_unavailability);

                    App.Http.Calendar.saveAppointment(unavailability).done(() => {
                        $notification.hide('blind');
                    });

                    info.revert();
                };

                App.Layouts.Backend.displayNotification(lang('unavailability_updated'), [
                    {
                        'label': lang('undo'),
                        'function': undoFunction
                    }
                ]);

                $footer.css('position', 'static'); // Footer position fix.

                // Update the event data for later use.
                info.event.setProp('data', info.event.extendedProps.data);
            };

            App.Http.Calendar.saveUnavailability(unavailability, successCallback, null);
        }
    }

    /**
     * Calendar Window "Resize" Callback
     *
     * The calendar element needs to be re-sized too in order to fit into the window. Nevertheless, if the window
     * becomes very small the calendar won't shrink anymore.
     *
     * @see getCalendarHeight()
     */
    function onWindowResize() {
        fullCalendar.setOption('height', getCalendarHeight());
    }

    /**
     * Calendar Day "Click" Callback
     *
     * When the user clicks on a day square on the calendar, then he will automatically be transferred to that
     * day view calendar.
     *
     * @param {Object} info
     */
    function onDateClick(info) {
        if (info.allDay) {
            fullCalendar.changeView('timeGridDay');
            fullCalendar.gotoDate(info.date);
        }
    }

    /**
     * Calendar Event "Drop" Callback
     *
     * This event handler is triggered whenever the user drags and drops an event into a different position
     * on the calendar. We need to update the database with this change. This is done via an ajax call.
     *
     * @param {Object} info
     */
    function onEventDrop(info) {
        if (vars('privileges').appointments.edit === false) {
            info.revert();
            App.Layouts.Backend.displayNotification(lang('no_privileges_edit_appointments'));
            return;
        }

        if ($notification.is(':visible')) {
            $notification.hide('bind');
        }

        let successCallback;

        if (!info.event.extendedProps.data.is_unavailability) {
            // Prepare appointment data.
            const appointment = {...info.event.extendedProps.data};

            // Must delete the following because only appointment data should be provided to the ajax call.
            delete appointment.customer;
            delete appointment.provider;
            delete appointment.service;

            appointment.start_datetime = moment(appointment.start_datetime)
                .add({days: info.delta.days, millisecond: info.delta.milliseconds})
                .format('YYYY-MM-DD HH:mm:ss');

            appointment.end_datetime = moment(appointment.end_datetime)
                .add({days: info.delta.days, millisecond: info.delta.milliseconds})
                .format('YYYY-MM-DD HH:mm:ss');

            appointment.is_unavailability = Number(appointment.is_unavailability);

            info.event.extendedProps.data.start_datetime = appointment.start_datetime;
            info.event.extendedProps.data.end_datetime = appointment.end_datetime;

            // Define success callback function.
            successCallback = () => {
                // Define the undo function, if the user needs to reset the last change.
                const undoFunction = () => {
                    appointment.start_datetime = moment(appointment.start_datetime)
                        .add({
                            days: -info.delta.days,
                            milliseconds: -info.delta.milliseconds
                        })
                        .format('YYYY-MM-DD HH:mm:ss');

                    appointment.end_datetime = moment(appointment.end_datetime)
                        .add({days: -info.delta.days, milliseconds: -info.delta.milliseconds})
                        .format('YYYY-MM-DD HH:mm:ss');

                    info.event.extendedProps.data.start_datetime = appointment.start_datetime;
                    info.event.extendedProps.data.end_datetime = appointment.end_datetime;

                    App.Http.Calendar.saveAppointment(appointment).done(() => {
                        $notification.hide('blind');
                    });

                    info.revert();
                };

                App.Layouts.Backend.displayNotification(lang('appointment_updated'), [
                    {
                        'label': lang('undo'),
                        'function': undoFunction
                    }
                ]);

                $footer.css('position', 'static'); // Footer position fix.
            };

            // Update appointment data.
            App.Http.Calendar.saveAppointment(appointment, null, successCallback);
        } else {
            // Update unavailability time period.
            const unavailability = {
                id: info.event.extendedProps.data.id,
                start_datetime: moment(info.event.start).format('YYYY-MM-DD HH:mm:ss'),
                end_datetime: moment(info.event.end).format('YYYY-MM-DD HH:mm:ss'),
                id_users_provider: info.event.extendedProps.data.id_users_provider
            };

            successCallback = () => {
                const undoFunction = () => {
                    unavailability.start_datetime = moment(unavailability.start_datetime)
                        .add({days: -info.delta.days, milliseconds: -info.delta.milliseconds})
                        .format('YYYY-MM-DD HH:mm:ss');

                    unavailability.end_datetime = moment(unavailability.end_datetime)
                        .add({days: -info.delta.days, milliseconds: -info.delta.milliseconds})
                        .format('YYYY-MM-DD HH:mm:ss');

                    unavailability.is_unavailability = Number(unavailability.is_unavailability);

                    info.event.extendedProps.data.start_datetime = unavailability.start_datetime;
                    info.event.extendedProps.data.end_datetime = unavailability.end_datetime;

                    App.Http.Calendar.saveUnavailability(unavailability).done(() => {
                        $notification.hide('blind');
                    });

                    info.revert();
                };

                App.Layouts.Backend.displayNotification(lang('unavailability_updated'), [
                    {
                        label: lang('undo'),
                        function: undoFunction
                    }
                ]);

                $footer.css('position', 'static'); // Footer position fix.
            };

            App.Http.Calendar.saveUnavailability(unavailability, successCallback);
        }
    }

    /**
     * Calendar "View Render" Callback
     *
     * Whenever the calendar changes or refreshes its view certain actions need to be made, in order to
     * display proper information to the user.
     */
    function onDatesSet() {
        if ($selectFilterItem.val() === null) {
            return;
        }

        refreshCalendarAppointments(
            $calendar,
            $selectFilterItem.val(),
            $('#select-filter-item option:selected').attr('type'),
            fullCalendar.view.currentStart,
            fullCalendar.view.currentEnd
        );

        $(window).trigger('resize'); // Places the footer on the bottom.

        // Remove all open popovers.
        $('.close-popover').each((index, closePopoverButton) => {
            if ($popoverTarget) {
                $popoverTarget.popover('dispose');
            }
        });

        // Add new popovers.
        $('.fv-events').each((index, eventEl) => {
            $(eventEl).popover();
        });
    }

    // /**
    //  * Convert titles to HTML
    //  *
    //  * On some calendar events the titles contain html markup that is not displayed properly due to the
    //  * FullCalendar plugin. This plugin sets the .fc-event-title value by using the $.text() method and
    //  * not the $.html() method. So in order for the title to display the html properly we convert all the
    //  * .fc-event-titles where needed into html.
    //  */
    // function convertTitlesToHtml() {
    //     // Convert the titles to html code.
    //     $('.fc-custom').each((index, customEventElement) => {
    //         const title = $(customEventElement).find('.fc-event-title').text();
    //         $(customEventElement).find('.fc-event-title').html(title);
    //         const time = $(customEventElement).find('.fc-event-time').text();
    //         $(customEventElement).find('.fc-event-time').html(time);
    //     });
    // }

    /**
     * Refresh Calendar Appointments
     *
     * This method reloads the registered appointments for the selected date period and filter type.
     *
     * @param {Object} $calendar The calendar jQuery object.
     * @param {Number} recordId The selected record id.
     * @param {String} filterType The filter type, could be either FILTER_TYPE_PROVIDER or FILTER_TYPE_SERVICE.
     * @param {String} startDate Visible start date of the calendar.
     * @param {String} endDate Visible end date of the calendar.
     */
    function refreshCalendarAppointments($calendar, recordId, filterType, startDate, endDate) {
        $('#loading').css('visibility', 'hidden');

        const calendarEventSource = [];

        startDate = moment(startDate).format('YYYY-MM-DD');

        endDate = moment(endDate).format('YYYY-MM-DD');

        App.Http.Calendar.getCalendarAppointments(recordId, startDate, endDate, filterType)
            .done((response) => {
                const calendarEventSources = fullCalendar.getEventSources();

                calendarEventSources.forEach((calendarEventSource) => calendarEventSource.remove());

                // Add appointments to calendar.
                response.appointments.forEach((appointment) => {
                    const appointmentEvent = {
                        id: appointment.id,
                        title:
                            appointment.service.name +
                            ' - ' +
                            appointment.customer.first_name +
                            ' ' +
                            appointment.customer.last_name,
                        start: moment(appointment.start_datetime).toDate(),
                        end: moment(appointment.end_datetime).toDate(),
                        allDay: false,
                        color: appointment.color,
                        data: appointment // Store appointment data for later use.
                    };

                    calendarEventSource.push(appointmentEvent);
                });

                // Add custom unavailability periods (they are always displayed on the calendar, even if the provider
                // won't work on that day).
                response.unavailabilities.forEach((unavailability) => {
                    let notes = unavailability.notes ? ' - ' + unavailability.notes : '';

                    if (unavailability.notes && unavailability.notes.length > 30) {
                        notes = unavailability.notes.substring(0, 30) + '...';
                    }

                    const unavailabilityEvent = {
                        title: lang('unavailability') + notes,
                        start: moment(unavailability.start_datetime).toDate(),
                        end: moment(unavailability.end_datetime).toDate(),
                        allDay: false,
                        color: '#879DB4',
                        editable: true,
                        className: 'fc-unavailability fc-custom',
                        data: unavailability
                    };

                    calendarEventSource.push(unavailabilityEvent);
                });

                const calendarView = fullCalendar.view;

                if (calendarView.type === 'dayGridMonth') {
                    return;
                }

                const provider = vars('available_providers').find(
                    (availableProvider) => Number(availableProvider.id) === Number(recordId)
                );

                const workingPlan = JSON.parse(provider ? provider.settings.working_plan : vars('company_working_plan'));
                const workingPlanExceptions = JSON.parse(provider ? provider.settings.working_plan_exceptions : '{}');
                let unavailabilityEvent;
                let viewStart;
                let viewEnd;
                let breakStart;
                let breakEnd;
                let workingPlanExceptionStart;
                let workingPlanExceptionEnd;
                let weekdayNumber;
                let weekdayName;
                let weekdayDate;
                let workingPlanExceptionEvent;
                let startHour;
                let endHour;
                let workDateStart;
                let workDateEnd;

                // Sort the working plan starting with the first day as set in General settings to correctly align
                // breaks in the calendar display.
                const firstWeekdayNumber = App.Utils.Date.getWeekdayId(vars('first_weekday'));
                const sortedWorkingPlan = App.Utils.Date.sortWeekDictionary(workingPlan, firstWeekdayNumber);

                switch (calendarView.type) {
                    case 'timeGridDay':
                        weekdayNumber = parseInt(moment(calendarView.currentStart).format('d'));
                        weekdayName = App.Utils.Date.getWeekdayName(weekdayNumber);
                        weekdayDate = moment(calendarView.currentStart).clone().format('YYYY-MM-DD');

                        // Add working plan exception.
                        if (workingPlanExceptions && workingPlanExceptions[weekdayDate]) {
                            sortedWorkingPlan[weekdayName] = workingPlanExceptions[weekdayDate];
                            workingPlanExceptionStart = weekdayDate + ' ' + sortedWorkingPlan[weekdayName].start;
                            workingPlanExceptionEnd = weekdayDate + ' ' + sortedWorkingPlan[weekdayName].end;

                            workingPlanExceptionEvent = {
                                title: lang('working_plan_exception'),
                                start: moment(workingPlanExceptionStart, 'YYYY-MM-DD HH:mm', true).toDate(),
                                end: moment(workingPlanExceptionEnd, 'YYYY-MM-DD HH:mm', true)
                                    .add(1, 'day')
                                    .toDate(),
                                allDay: true,
                                color: '#879DB4',
                                editable: false,
                                className: 'fc-working-plan-exception fc-custom',
                                data: {
                                    date: weekdayDate,
                                    workingPlanException: workingPlanExceptions[weekdayDate],
                                    provider: provider
                                }
                            };

                            calendarEventSource.push(workingPlanExceptionEvent);
                        }

                        // Non-working day.
                        if (sortedWorkingPlan[weekdayName] === null) {
                            // Working plan exception.
                            unavailabilityEvent = {
                                title: lang('not_working'),
                                start: calendarView.currentStart.clone().toDate(),
                                end: calendarView.currentEnd.clone().toDate(),
                                allDay: false,
                                color: '#BEBEBE',
                                editable: false,
                                className: 'fc-unavailability'
                            };

                            calendarEventSource.push(unavailabilityEvent);

                            return; // Go to next loop.
                        }

                        // Add unavailability period before work starts.
                        viewStart = moment(calendarView.currentStart).format('YYYY-MM-DD') + ' 00:00:00';
                        startHour = sortedWorkingPlan[weekdayName].start.split(':');
                        workDateStart = moment(viewStart).clone();
                        workDateStart.hour(parseInt(startHour[0]));
                        workDateStart.minute(parseInt(startHour[1]));

                        if (moment(viewStart).isBefore(workDateStart)) {
                            const unavailabilityPeriodBeforeWorkStarts = {
                                title: lang('not_working'),
                                start: moment(viewStart).toDate(),
                                end: workDateStart.toDate(),
                                allDay: false,
                                color: '#BEBEBE',
                                editable: false,
                                className: 'fc-unavailability'
                            };

                            calendarEventSource.push(unavailabilityPeriodBeforeWorkStarts);
                        }

                        // Add unavailability period after work ends.
                        viewEnd = moment(calendarView.currentEnd).format('YYYY-MM-DD') + ' 00:00:00';
                        endHour = sortedWorkingPlan[weekdayName].end.split(':');
                        workDateEnd = moment(viewStart).clone();
                        workDateEnd.hour(parseInt(endHour[0]));
                        workDateEnd.minute(parseInt(endHour[1]));

                        if (moment(viewEnd).isAfter(workDateEnd)) {
                            const unavailabilityPeriodAfterWorkEnds = {
                                title: lang('not_working'),
                                start: workDateEnd.toDate(),
                                end: moment(viewEnd).toDate(),
                                allDay: false,
                                color: '#BEBEBE',
                                editable: false,
                                className: 'fc-unavailability'
                            };

                            calendarEventSource.push(unavailabilityPeriodAfterWorkEnds);
                        }

                        // Add unavailability periods for breaks.
                        sortedWorkingPlan[weekdayName].breaks.forEach((breakPeriod) => {
                            const breakStartString = breakPeriod.start.split(':');
                            breakStart = moment(viewStart).clone();
                            breakStart.hour(parseInt(breakStartString[0]));
                            breakStart.minute(parseInt(breakStartString[1]));

                            const breakEndString = breakPeriod.end.split(':');
                            breakEnd = moment(viewStart).clone();
                            breakEnd.hour(parseInt(breakEndString[0]));
                            breakEnd.minute(parseInt(breakEndString[1]));

                            const unavailabilityPeriod = {
                                title: lang('break'),
                                start: breakStart.toDate(),
                                end: breakEnd.toDate(),
                                allDay: false,
                                color: '#BEBEBE',
                                editable: false,
                                className: 'fc-unavailability fc-break'
                            };

                            calendarEventSource.push(unavailabilityPeriod);
                        });

                        break;

                    case 'timeGridWeek':
                        const calendarDate = moment(calendarView.currentStart).clone();

                        while (calendarDate.toDate() < calendarView.currentEnd) {
                            weekdayNumber = parseInt(calendarDate.format('d'));
                            weekdayName = App.Utils.Date.getWeekdayName(weekdayNumber);
                            weekdayDate = calendarDate.format('YYYY-MM-DD');

                            // Add working plan exception event.
                            if (workingPlanExceptions && workingPlanExceptions[weekdayDate]) {
                                sortedWorkingPlan[weekdayName] = workingPlanExceptions[weekdayDate];

                                workingPlanExceptionStart =
                                    weekdayDate + ' ' + sortedWorkingPlan[weekdayName].start;
                                workingPlanExceptionEnd = weekdayDate + ' ' + sortedWorkingPlan[weekdayName].end;

                                workingPlanExceptionEvent = {
                                    title: lang('working_plan_exception'),
                                    start: moment(workingPlanExceptionStart, 'YYYY-MM-DD HH:mm', true).toDate(),
                                    end: moment(workingPlanExceptionEnd, 'YYYY-MM-DD HH:mm', true)
                                        .add(1, 'day')
                                        .toDate(),
                                    allDay: true,
                                    color: '#879DB4',
                                    editable: false,
                                    className: 'fc-working-plan-exception fc-custom',
                                    data: {
                                        date: weekdayDate,
                                        workingPlanException: workingPlanExceptions[weekdayDate],
                                        provider: provider
                                    }
                                };

                                calendarEventSource.push(workingPlanExceptionEvent);
                            }

                            // Non-working day.
                            if (sortedWorkingPlan[weekdayName] === null) {
                                // Add a full day unavailability event.
                                unavailabilityEvent = {
                                    title: lang('not_working'),
                                    start: calendarDate.clone().toDate(),
                                    end: calendarDate.clone().add(1, 'day').toDate(),
                                    allDay: false,
                                    color: '#BEBEBE',
                                    editable: false,
                                    className: 'fc-unavailability'
                                };

                                calendarEventSource.push(unavailabilityEvent);

                                calendarDate.add(1, 'day');

                                continue; // Go to the next loop.
                            }

                            // Add unavailability period before work starts.
                            startHour = sortedWorkingPlan[weekdayName].start.split(':');
                            workDateStart = calendarDate.clone();
                            workDateStart.hour(parseInt(startHour[0]));
                            workDateStart.minute(parseInt(startHour[1]));

                            if (calendarDate.toDate() < workDateStart.toDate()) {
                                unavailabilityEvent = {
                                    title: lang('not_working'),
                                    start: calendarDate.clone().toDate(),
                                    end: moment(
                                        calendarDate.format('YYYY-MM-DD') +
                                        ' ' +
                                        sortedWorkingPlan[weekdayName].start +
                                        ':00'
                                    ).toDate(),
                                    allDay: false,
                                    color: '#BEBEBE',
                                    editable: false,
                                    className: 'fc-unavailability'
                                };

                                calendarEventSource.push(unavailabilityEvent);
                            }

                            // Add unavailability period after work ends.
                            endHour = sortedWorkingPlan[weekdayName].end.split(':');
                            workDateEnd = calendarDate.clone();
                            workDateEnd.hour(parseInt(endHour[0]));
                            workDateEnd.minute(parseInt(endHour[1]));

                            if (calendarView.currentEnd > workDateEnd.toDate()) {
                                unavailabilityEvent = {
                                    title: lang('not_working'),
                                    start: moment(
                                        calendarDate.format('YYYY-MM-DD') +
                                        ' ' +
                                        sortedWorkingPlan[weekdayName].end +
                                        ':00'
                                    ).toDate(),
                                    end: calendarDate.clone().add(1, 'day').toDate(),
                                    allDay: false,
                                    color: '#BEBEBE',
                                    editable: false,
                                    className: 'fc-unavailability'
                                };

                                calendarEventSource.push(unavailabilityEvent);
                            }

                            // Add unavailability periods during day breaks.
                            sortedWorkingPlan[weekdayName].breaks.forEach((breakPeriod) => {
                                const breakStartString = breakPeriod.start.split(':');
                                breakStart = calendarDate.clone();
                                breakStart.hour(parseInt(breakStartString[0]));
                                breakStart.minute(parseInt(breakStartString[1]));

                                const breakEndString = breakPeriod.end.split(':');
                                breakEnd = calendarDate.clone();
                                breakEnd.hour(parseInt(breakEndString[0]));
                                breakEnd.minute(parseInt(breakEndString[1]));

                                const unavailabilityEvent = {
                                    title: lang('break'),
                                    start: moment(
                                        calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.start
                                    ).toDate(),
                                    end: moment(calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.end).toDate(),
                                    allDay: false,
                                    color: '#BEBEBE',
                                    editable: false,
                                    className: 'fc-unavailability fc-break'
                                };

                                calendarEventSource.push(unavailabilityEvent);
                            });

                            calendarDate.add(1, 'day');
                        }

                        break;
                }
            })
            .always(() => {
                $('#loading').css('visibility', '');
                fullCalendar.addEventSource(calendarEventSource);
            });
    }

    function initialize() {
        // Dynamic date formats.
        let columnFormat = {};

        switch (vars('date_format')) {
            case 'DMY':
                columnFormat = 'ddd D/M';
                break;

            case 'MDY':
            case 'YMD':
                columnFormat = 'ddd M/D';
                break;

            default:
                throw new Error('Invalid date format setting provided!', vars('date_format'));
        }

        // Time formats
        let timeFormat = '';
        let slotTimeFormat = '';

        switch (vars('time_format')) {
            case 'military':
                timeFormat = 'H:mm';
                slotTimeFormat = 'H';
                break;
            case 'regular':
                timeFormat = 'h:mm a';
                slotTimeFormat = 'h a';
                break;
            default:
                throw new Error('Invalid time format setting provided!', vars('time_format'));
        }

        const initialView = window.innerWidth < 468 ? 'timeGridDay' : 'timeGridWeek';

        const firstWeekday = vars('first_weekday');
        const firstWeekdayNumber = App.Utils.Date.getWeekdayId(firstWeekday);

        // Initialize page calendar
        fullCalendar = new FullCalendar.Calendar($calendar[0], {
            initialView,
            height: getCalendarHeight(),
            editable: true,
            firstDay: firstWeekdayNumber,
            slotDuration: '00:15:00',
            snapDuration: '00:15:00',
            slotLabelInterval: '01:00',
            eventTimeFormat: timeFormat,
            eventTextColor: '#333',
            slotLabelFormat: slotTimeFormat,
            allDayContent: lang('all_day'),
            dayHeaderFormat: columnFormat,
            headerToolbar: {
                left: 'prev,next today',
                center: 'title',
                right: 'timeGridDay,timeGridWeek,dayGridMonth'
            },

            // Selectable
            selectable: true,
            selectMirror: true,
            select: (info) => {
                if (info.allDay) {
                    return;
                }

                const isProviderDisplayed =
                    $selectFilterItem.find('option:selected').attr('type') === FILTER_TYPE_PROVIDER;

                const buttons = [
                    {
                        text: lang('close'),
                        click: () => {
                            $('#message-box').dialog('close');
                        }
                    },
                    {
                        text: lang('unavailability'),
                        click: () => {
                            $('#insert-unavailability').trigger('click');

                            if (isProviderDisplayed) {
                                $('#unavailability-provider').val($selectFilterItem.val());
                            } else {
                                $('#unavailability-provider option:first').prop('selected', true);
                            }

                            $('#unavailability-provider').trigger('change');

                            $('#unavailability-start').datepicker('setDate', info.start);

                            $('#unavailability-end').datepicker('setDate', info.end);

                            $('#message-box').dialog('close');
                        }
                    },
                    {
                        text: lang('appointment'),
                        click: () => {
                            $('#insert-appointment').trigger('click');

                            // Preselect service & provider.
                            let service;

                            if (isProviderDisplayed) {
                                const provider = vars('available_providers').find(
                                    (availableProvider) =>
                                        Number(availableProvider.id) === Number($selectFilterItem.val())
                                );

                                if (provider) {
                                    service = vars('available_services').find(
                                        (availableService) => provider.services.indexOf(availableService.id) !== -1
                                    );

                                    if (service) {
                                        $appointmentsModal.find('#select-service').val(service.id);
                                    }
                                }

                                if (!$appointmentsModal.find('#select-service').val()) {
                                    $('#select-service option:first').prop('selected', true);
                                }

                                $appointmentsModal.find('#select-service').trigger('change');

                                if (provider) {
                                    $appointmentsModal.find('#select-provider').val(provider.id);
                                }

                                if (!$appointmentsModal.find('#select-provider').val()) {
                                    $appointmentsModal.find('#select-provider option:first').prop('selected', true);
                                }

                                $appointmentsModal.find('#select-provider').trigger('change');
                            } else {
                                service = vars('available_services').find(
                                    (availableService) =>
                                        Number(availableService.id) === Number($selectFilterItem.val())
                                );

                                if (service) {
                                    $appointmentsModal.find('#select-service').val(service.id).trigger('change');
                                }
                            }

                            // Preselect time
                            $('#start-datetime').datepicker('setDate', info.start);
                            $('#end-datetime').datepicker('setDate', App.Pages.Calendar.getSelectionEndDate(info));

                            // $('#end-datetime').datepicker('setDate', info.end);
                            $('#message-box').dialog('close');
                        }
                    }
                ];

                App.Utils.Message.show(lang('add_new_event'), lang('what_kind_of_event'), buttons);

                return false;
            },

            buttonText: {
                today: lang('today'),
                day: lang('day'),
                week: lang('week'),
                month: lang('month')
            },

            // Calendar events need to be declared on initialization.
            windowResize: onWindowResize,
            datesSet: onDatesSet,
            dateClick: onDateClick,
            eventClick: onEventClick,
            eventResize: onEventResize,
            eventDrop: onEventDrop
        });

        fullCalendar.render();

        // Trigger once to set the proper footer position after calendar initialization.
        onWindowResize();

        $selectFilterItem.append(new Option(lang('all'), FILTER_TYPE_ALL, true, true));

        $('#insert-working-plan-exception').hide();

        // Fill the select list boxes of the page.
        if (vars('available_providers').length > 0) {
            $('<optgroup/>', {
                'label': lang('providers'),
                'type': 'providers-group',
                'html': vars('available_providers').map((availableProvider) => {
                    const hasGoogleSync = availableProvider.settings.google_sync === '1' ? 'true' : 'false';

                    return $('<option/>', {
                        'value': availableProvider.id,
                        'type': FILTER_TYPE_PROVIDER,
                        'google-sync': hasGoogleSync,
                        'text': availableProvider.first_name + ' ' + availableProvider.last_name
                    });
                })
            }).appendTo('#select-filter-item');
        }

        if (vars('available_services').length > 0) {
            $('<optgroup/>', {
                'label': lang('services'),
                'type': 'services-group',
                'html': vars('available_services').map((availableService) =>
                    $('<option/>', {
                        'value': availableService.id,
                        'type': FILTER_TYPE_SERVICE,
                        'text': availableService.name
                    })
                )
            }).appendTo('#select-filter-item');
        }

        // Check permissions.
        if (vars('role_slug') === App.Layouts.Backend.DB_SLUG_PROVIDER) {
            $selectFilterItem
                .find('optgroup:eq(0)')
                .find('option[value="' + vars('user_id') + '"]')
                .prop('selected', true);
        }

        // Add the page event listeners.
        addEventListeners();

        $reloadAppointments.trigger('click');

        // Display the edit dialog if an appointment hash is provided.
        if (vars('edit_appointment')) {
            const appointment = vars('edit_appointment');

            App.Components.AppointmentsModal.resetModal();

            $appointmentsModal.find('.modal-header h3').text(lang('edit_appointment_title'));
            $appointmentsModal.find('#appointment-id').val(appointment.id);
            $appointmentsModal.find('#select-service').val(appointment.id_services).trigger('change');
            $appointmentsModal.find('#select-provider').val(appointment.id_users_provider);

            // Set the start and end datetime of the appointment.
            const startDatetimeMoment = moment(appointment.start_datetime);
            $appointmentsModal.find('#start-datetime').datetimepicker('setDate', startDatetimeMoment.toDate());

            const endDatetimeMoment = moment(appointment.end_datetime);
            $appointmentsModal.find('#end-datetime').datetimepicker('setDate', endDatetimeMoment.toDate());

            const customer = appointment.customer;
            $appointmentsModal.find('#customer-id').val(appointment.id_users_customer);
            $appointmentsModal.find('#first-name').val(customer.first_name);
            $appointmentsModal.find('#last-name').val(customer.last_name);
            $appointmentsModal.find('#email').val(customer.email);
            $appointmentsModal.find('#phone-number').val(customer.phone_number);
            $appointmentsModal.find('#address').val(customer.address);
            $appointmentsModal.find('#city').val(customer.city);
            $appointmentsModal.find('#zip-code').val(customer.zip_code);
            $appointmentsModal.find('#language').val(customer.language);
            $appointmentsModal.find('#timezone').val(customer.timezone);
            $appointmentsModal.find('#appointment-location').val(appointment.location);
            $appointmentsModal.find('#appointment-notes').val(appointment.notes);
            $appointmentsModal.find('#customer-notes').val(customer.notes);

            App.Components.ColorSelection.setColor($appointmentsModal.find('#appointment-color'), appointment.color);

            $appointmentsModal.modal('show');

            fullCalendar.gotoDate(moment(appointment.start_datetime).toDate());
        }

        if (!$selectFilterItem.find('option').length) {
            $('#calendar-actions button').prop('disabled', true);
        }

        // Fine tune the footer's position only for this page.
        if (window.innerHeight < 700) {
            $footer.css('position', 'static');
        }

        // Automatically refresh the calendar page every 10 seconds (without loading animation).
        setInterval(() => {
            if ($('.popover').length) {
                return;
            }

            refreshCalendarAppointments(
                $calendar,
                $selectFilterItem.val(),
                $selectFilterItem.find('option:selected').attr('type'),
                fullCalendar.view.currentStart,
                fullCalendar.view.currentEnd
            );
        }, 60000);

        $reloadAppointments.trigger('click');
    }

    return {
        initialize,
        FILTER_TYPE_ALL,
        FILTER_TYPE_PROVIDER,
        FILTER_TYPE_SERVICE
    };
})();