Source: frontend_book.js

/**
 * This namespace contains functions that implement the book appointment page 
 * functionality. Once the initialize() method is called the page is fully 
 * functional and can serve the appointment booking process.
 * 
 * @namespace FrontendBook
 */
var FrontendBook = {
    /**
     * Determines the functionality of the page.
     * 
     * @type {bool}
     */
    manageMode: false,  
    
    /**
     * This method initializes the book appointment page.
     * 
     * @param {bool} bindEventHandlers (OPTIONAL) Determines whether the default   
     * event handlers will be binded to the dom elements.
     * @param {bool} manageMode (OPTIONAL) Determines whether the customer is going 
     * to make  changes to an existing appointment rather than booking a new one.
     */
    initialize: function(bindEventHandlers, manageMode) {
        if (bindEventHandlers === undefined) {
            bindEventHandlers = true; // Default Value
        }
        
        if (manageMode === undefined) {
            manageMode = false; // Default Value
        }
        
        FrontendBook.manageMode = manageMode;
        
        // Initialize page's components (tooltips, datepickers etc).
        $('.book-step').qtip({
            position: {
                my: 'top center',
                at: 'bottom center'
            },
            style: {
                classes: 'qtip-green qtip-shadow custom-qtip'
            }
        });
        
        $('#select-date').datepicker({
            dateFormat: 'dd-mm-yy',
            firstDay: 1, // Monday
            minDate: 0,
            defaultDate: Date.today(),
            onSelect: function(dateText, instance) {
                FrontendBook.getAvailableHours(dateText);
                FrontendBook.updateConfirmFrame();
            }
        });
       
        // Bind the event handlers (might not be necessary every time
        // we use this class).
        if (bindEventHandlers) {
            FrontendBook.bindEventHandlers();
        }
        
        // If the manage mode is true, the appointments data should be
        // loaded by default.
        if (FrontendBook.manageMode) {
            FrontendBook.applyAppointmentData(GlobalVariables.appointmentData,
                    GlobalVariables.providerData, GlobalVariables.customerData);
        } else {
            $('#select-service').trigger('change'); // Load the available hours.
        }
    },
    
    /**
     * This method binds the necessary event handlers for the book 
     * appointments page.
     */
    bindEventHandlers: function() {
        /**
         * Event: Selected Provider "Changed"
         * 
         * Whenever the provider changes the available appointment
         * date - time periods must be updated.
         */
        $('#select-provider').change(function() {
            FrontendBook.getAvailableHours(Date.today().toString('dd-MM-yyyy'));
            FrontendBook.updateConfirmFrame();
        });
        
        /**
         * Event: Selected Service "Changed"
         * 
         * When the user clicks on a service, its available providers should 
         * become visible. 
         */
        $('#select-service').change(function() {
            var currServiceId = $('#select-service').val();
            $('#select-provider').empty();

            $.each(GlobalVariables.availableProviders, function(indexProvider, provider) {
                $.each(provider['services'], function(indexService, serviceId) {
                    // If the current provider is able to provide the selected service,
                    // add him to the listbox. 
                    if (serviceId == currServiceId) { 
                        var optionHtml = '<option value="' + provider['id'] + '">' 
                                + provider['last_name']  + ' ' + provider['first_name'] 
                                + '</option>';
                        $('#select-provider').append(optionHtml);
                    }
                });
            });

            FrontendBook.getAvailableHours(Date.today().toString('dd-MM-yyyy'));
            FrontendBook.updateConfirmFrame();
            FrontendBook.updateServiceDescription($('#select-service').val(), $('#service-description'));
        });
        
        /**
         * Event: Next Step Button "Clicked"
         * 
         * This handler is triggered every time the user pressed the 
         * "next" button on the book wizard. Some special tasks might 
         * be perfomed, depending the current wizard step.
         */
        $('.button-next').click(function() {
            // If we are on the 2nd tab then the user should have an appointment hour 
            // selected.
            if ($(this).attr('data-step_index') === '2') {
                if ($('.selected-hour').length == 0) {
                    if ($('#select-hour-prompt').length == 0) {
                        $('#available-hours').append('<br><br>'
                                + '<strong id="select-hour-prompt" class="text-error">'
                                + 'Please select an appointment hour before continuing!' 
                                + '</strong>');
                    }
                    return;
                }
            }
            
            // If we are on the 3rd tab then we will need to validate the user's 
            // input before proceeding to the next step.
            if ($(this).attr('data-step_index') === '3') {
                if (!FrontendBook.validateCustomerForm()) {
                    return; // Validation failed, do not continue.
                } else {
                    FrontendBook.updateConfirmFrame();
                }
            }
            
            // Display the next step tab (uses jquery animation effect).
            var nextTabIndex = parseInt($(this).attr('data-step_index')) + 1;

            $(this).parents().eq(1).hide('fade', function() {    
                $('.active-step').removeClass('active-step');
                $('#step-' + nextTabIndex).addClass('active-step');
                $('#wizard-frame-' + nextTabIndex).show('fade');
            });
        });

        /**
         * Event: Back Step Button "Clicked"
         * 
         * This handler is triggered every time the user pressed the 
         * "back" button on the book wizard.
         */
        $('.button-back').click(function() {
            var prevTabIndex = parseInt($(this).attr('data-step_index')) - 1;

            $(this).parents().eq(1).hide('fade', function() {    
                $('.active-step').removeClass('active-step');
                $('#step-' + prevTabIndex).addClass('active-step');
                $('#wizard-frame-' + prevTabIndex).show('fade');
            });
        });

        /**
         * Event: Available Hour "Click"
         * 
         * Triggered whenever the user clicks on an available hour
         * for his appointment.
         */
        $('#available-hours').on('click', '.available-hour', function() {
            $('.selected-hour').removeClass('selected-hour');
            $(this).addClass('selected-hour');
            FrontendBook.updateConfirmFrame();
        });
        
        if (FrontendBook.manageMode) {
            /**
             * Event: Cancel Appointment Button "Click"
             * 
             * When the user clicks the "Cancel" button this form is going to
             * be submitted. We need the user to confirm this action because
             * once the appointment is cancelled, it will be delete from the 
             * database.
             */
            $('#cancel-appointment').click(function() {
                event.preventDefault();
                
                var dialogButtons = {
                    'OK': function() {
                        if ($('#cancel-reason').val() === '') {
                            $('#cancel-reason').css('border', '2px solid red');
                            return;
                        }
                        $('#cancel-appointment-form textarea').val($('#cancel-reason').val());
                        $('#cancel-appointment-form').submit();
                    },
                    'Cancel': function() {
                        $('#message_box').dialog('close');
                    }
                };
                
                GeneralFunctions.displayMessageBox('Cancel Appointment', 'Please take a '
                        + 'minute to write the reason you are cancelling the appointment:',
                        dialogButtons);
                        
                $('#message_box').append('<textarea id="cancel-reason"></textarea>');
                $('#cancel-reason').css('width', '300px');
            });
        }
        
        /**
         * Event: Book Appointment Form "Submit"
         * 
         * Before the form is submitted to the server we need to make sure that
         * in the meantime the selected appointment date/time wasn't reserved by
         * another customer or event. 
         */
        $('#book-appointment-form').submit(function() {
            event.preventDefault();
            
            var formData = jQuery.parseJSON($('input[name="post_data"]').val());
            
            var postData = {
                'id_users_provider': formData['appointment']['id_users_provider'],
                'id_services': formData['appointment']['id_services'],
                'start_datetime': formData['appointment']['start_datetime']
            };
            
            var postUrl = GlobalVariables.baseUrl + 'appointments/ajax_check_datetime_availability';
            
            $.post(postUrl, postData, function(response) {
                ////////////////////////////////////////////////////////////////////////
                console.log('Check Date/Time Availability Post Response :', response);
                ////////////////////////////////////////////////////////////////////////
                
                if (response.exceptions) {
                    response.exceptions = GeneralFunctions.parseExceptions(response.exceptions);
                    GeneralFunctions.displayMessageBox('Unexpected Issues', 'Unfortunately '
                            + 'the check appointment time availability could not be completed. '
                            + 'The following issues occured:');
                    $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions));
                    return;
                } 
                
                if (response === true) {
                    $('#book-appointment-form').submit();
                } else {
                    GeneralFunctions.displayMessageBox('Appointment Hour Taken', 'Unfortunately '
                            + 'the selected appointment hour is not available anymore. Please select '
                            + 'another hour.');
                    FrontendBook.getAvailableHours($('#select-date').val());
                }
            }, 'json');
        });
    },
    
    /**
     * This function makes an ajax call and returns the available 
     * hours for the selected service, provider and date.
     * 
     * @param {string} selDate The selected date of which the available
     * hours we need to receive.
     */
    getAvailableHours: function(selDate) {
        $('#available-hours').empty();
        
        // Find the selected service duration (it is going to 
        // be send within the "postData" object.
        var selServiceDuration = 15; // Default value of duration (in minutes).
        $.each(GlobalVariables.availableServices, function(index, service) {
            if (service['id'] == $('#select-service').val()) {
                selServiceDuration = service['duration']; 
            }
        });
        
        // If the manage mode is true then the appointment's start 
        // date should return as available too.
        var appointmentId = (FrontendBook.manageMode) 
                ? GlobalVariables.appointmentData['id'] : undefined;

        var postData = {
            'service_id': $('#select-service').val(),
            'provider_id': $('#select-provider').val(),
            'selected_date': selDate,
            'service_duration': selServiceDuration,
            'manage_mode': FrontendBook.manageMode,
            'appointment_id': appointmentId
        }

        // Make ajax post request and get the available hours.
        var ajaxurl = GlobalVariables.baseUrl + 'appointments/ajax_get_available_hours';
        jQuery.post(ajaxurl, postData, function(response) {
            ///////////////////////////////////////////////////////////////
            //console.log('Get Available Hours JSON Response:', response);
            ///////////////////////////////////////////////////////////////

            if (!GeneralFunctions.handleAjaxExceptions(response)) return;

            // The response contains the available hours for the selected provider and
            // service. Fill the available hours div with response data. 
            if (response.length > 0) {
                var currColumn = 1;
                $('#available-hours').html('<div style="width:50px; float:left;"></div>');

                $.each(response, function(index, availableHour) {
                    if ((currColumn * 10) < (index + 1)) {
                        currColumn++;
                        $('#available-hours').append('<div style="width:50px; float:left;"></div>');
                    }

                    $('#available-hours div:eq(' + (currColumn - 1) + ')').append(
                            '<span class="available-hour">' + availableHour + '</span><br/>');
                });

                if (FrontendBook.manageMode) {
                    // Set the appointment's start time as the default selection.
                    $('.available-hour').removeClass('selected-hour');
                    $('.available-hour').filter(function() {
                        return $(this).text() === Date.parseExact(
                                GlobalVariables.appointmentData['start_datetime'],
                                'yyyy-MM-dd HH:mm:ss').toString('HH:mm');
                    }).addClass('selected-hour');
                } else {
                    // Set the first available hour as the default selection.
                    $('.available-hour:eq(0)').addClass('selected-hour');
                }

                FrontendBook.updateConfirmFrame();
                
            } else {
                $('#available-hours').text('There are no available appointment '
                        + 'hours for the selected date. Please choose another date.');
            }
        }, 'json');
    },

    /**
     * This function validates the customer's data input. The user cannot contiue
     * without passing all the validation checks.
     * 
     * @return {bool} Returns the validation result.
     */
    validateCustomerForm: function() {
        $('#wizard-frame-3 input').css('border', '');
        
        try {
            // Validate required fields.
            var missingRequiredField = false;
            $('.required').each(function() {
                if ($(this).val() == '') {
                    $(this).css('border', '2px solid red');
                    missingRequiredField = true;
                }
            });
            if (missingRequiredField) {
                throw 'Fields with * are required!';
            }
            
            // Validate email address.
            if (!GeneralFunctions.validateEmail($('#email').val())) {
                $('#email').css('border', '2px solid red');
                throw 'Invalid email address!';
            }
            
            return true;
        } catch(exc) {
            $('#form-message').text(exc);
            return false;
        }
    },

    /**
     * Every time this function is executed, it updates the confirmation
     * page with the latest customer settigns and input for the appointment 
     * booking.
     */
    updateConfirmFrame: function() {
        // Appointment Details
        var selectedDate = $('#select-date').datepicker('getDate');
        if (selectedDate !== null) {
            selectedDate = Date.parse(selectedDate).toString('dd/MM/yyyy');
        }

        var selServiceId = $('#select-service').val();
        var servicePrice, serviceCurrency;
        $.each(GlobalVariables.availableServices, function(index, service) {
            if (service.id == selServiceId) {
                servicePrice = '<br>' + service.price;
                serviceCurrency = service.currency;
                return false; // break loop
            }
        });

        $('#appointment-details').html(
            '<h4>' + $('#select-service option:selected').text() + '</h4>' +  
            '<p>' 
        		+ '<strong class="text-info">' 
                    + $('#select-provider option:selected').text() + '<br>'
        			+ selectedDate + ' ' +  $('.selected-hour').text() 
                    + servicePrice + ' ' + serviceCurrency
    			+ '</strong>' + 
            '</p>'
        );

        // Customer Details
        $('#customer-details').html(
            '<h4>' + $('#first-name').val() + ' ' + $('#last-name').val() + '</h4>' + 
            '<p>' + 
            	'Phone: ' + $('#phone-number').val() + 
            	'<br/>' + 
            	'Email: ' + $('#email').val() + 
            	'<br/>' + 
            	'Address: ' + $('#address').val() + 
            	'<br/>' + 
            	'City: ' + $('#city').val() + 
            	'<br/>' + 
            	'Zip Code: ' + $('#zip-code').val() + 
        	'</p>'
        );
            
        // Update appointment form data for submission to server when the user confirms
        // the appointment.
        var postData = new Object();
        
        postData['customer'] = {
            'last_name': $('#last-name').val(),
            'first_name': $('#first-name').val(),
            'email': $('#email').val(),
            'phone_number': $('#phone-number').val(),
            'address': $('#address').val(),
            'city': $('#city').val(),
            'zip_code': $('#zip-code').val()
        };
        
        postData['appointment'] = {
            'start_datetime': $('#select-date').datepicker('getDate').toString('yyyy-MM-dd') 
                                    + ' ' + $('.selected-hour').text() + ':00',
            'end_datetime': FrontendBook.calcEndDatetime(),
            'notes': $('#notes').val(),
            'is_unavailable': false,
            'id_users_provider': $('#select-provider').val(),
            'id_services': $('#select-service').val()
        };
        
        postData['manage_mode'] = FrontendBook.manageMode;
        
        if (FrontendBook.manageMode) {
            postData['appointment']['id'] = GlobalVariables.appointmentData['id'];
            postData['customer']['id'] = GlobalVariables.customerData['id'];
        }
        
        $('input[name="post_data"]').val(JSON.stringify(postData));
    },
    
    /** 
     * This method calculates the end datetime of the current appointment. 
     * End datetime is depending on the service and start datetime fieldss.
     * 
     * @return {string} Returns the end datetime in string format.
     */
    calcEndDatetime: function() {
        // Find selected service duration. 
        var selServiceDuration = undefined;
        
        $.each(GlobalVariables.availableServices, function(index, service) {
            if (service.id == $('#select-service').val()) {
                selServiceDuration = service.duration;
                return false; // Stop searching ... 
            }
        });
        
        // Add the duration to the start datetime.
        var startDatetime = $('#select-date').datepicker('getDate').toString('dd-MM-yyyy') 
                + ' ' + $('.selected-hour').text();
        startDatetime = Date.parseExact(startDatetime, 'dd-MM-yyyy HH:mm');
        var endDatetime = undefined;
        
        if (selServiceDuration !== undefined && startDatetime !== null) {
            endDatetime = startDatetime.add({ 'minutes' : parseInt(selServiceDuration) });
        } else {
            endDatetime = new Date();
        }
        
        return endDatetime.toString('yyyy-MM-dd HH:mm:ss');
    },
    
    /**
     * This method applies the appointment's data to the wizard so 
     * that the user can start making changes on an existing record.
     * 
     * @param {object} appointment Selected appointment's data.
     * @param {object} provider Selected provider's data.
     * @param {object} customer Selected customer's data.
     * @returns {bool} Returns the operation result.
     */
    applyAppointmentData: function(appointment, provider, customer) {
        try {
            // Select Service & Provider
            $('#select-service').val(appointment['id_services']).trigger('change');
            $('#select-provider').val(appointment['id_users_provider']);
            
            // Set Appointment Date
            $('#select-date').datepicker('setDate', Date.parseExact(
                    appointment['start_datetime'], 'yyyy-MM-dd HH:mm:ss'));
            FrontendBook.getAvailableHours($('#select-date').val());
            
            // Apply Customer's Data
            $('#last-name').val(customer['last_name']);
            $('#first-name').val(customer['first_name']);
            $('#email').val(customer['email']);
            $('#phone-number').val(customer['phone_number']);
            $('#address').val(customer['address']);
            $('#city').val(customer['city']);
            $('#zip-code').val(customer['zip_code']);
            var appointmentNotes = (appointment['notes'] !== null) 
                    ? appointment['notes'] : '';
            $('#notes').val(appointmentNotes);
            
            FrontendBook.updateConfirmFrame();
            
            return true;
        } catch(exc) {
            console.log(exc); // log exception
            return false;
        }
    },
    
    /**
     * This method updates a div's html content with a brief description of the 
     * user selected service (only if available in db). This is usefull for the 
     * customers upon selecting the correct service.
     * 
     * @param {int} serviceId The selected service record id.
     * @param {object} $div The destination div jquery object (e.g. provide $('#div-id') 
     * object as value).
     */
    updateServiceDescription: function(serviceId, $div) {
        var html = ''; 
        
        $.each(GlobalVariables.availableServices, function(index, service) {
            if (service.id == serviceId) { // Just found the service.
                html = '<strong>' + service.name + ':</strong> ';
                
                if (service.description != '' && service.description != null) {
                    html += service.description;
                }
                
                if (service.price != '' && service.price != null) {
                    html += ' [Price ' + service.price + ' ' + service.currency  + ']';
                }   
                
                html += '<br>';
                
                return false;
            }
        });
        
        $div.html(html);
        
        if (html != '') {
            $div.show();
        } else {
            $div.hide();
        }
        
    }
    
};