From f0c4f6f12b0d24bd916dbba858cedb0d35b7da34 Mon Sep 17 00:00:00 2001 From: Alex Tselegidis Date: Mon, 23 Nov 2015 22:58:32 +0100 Subject: [PATCH] Fixes #31 --- src/application/controllers/appointments.php | 433 +++++++++--------- .../language/english/translations_lang.php | 1 + src/assets/js/frontend_book.js | 46 +- 3 files changed, 219 insertions(+), 261 deletions(-) diff --git a/src/application/controllers/appointments.php b/src/application/controllers/appointments.php index 0772477b..3f475430 100755 --- a/src/application/controllers/appointments.php +++ b/src/application/controllers/appointments.php @@ -216,6 +216,40 @@ class Appointments extends CI_Controller { $this->load->view('appointments/message', $view); } + /** + * GET an specific appointment book and redirect to the success screen. + * + * @param int $appointment_id Contains the id of the appointment to retrieve. + */ + public function book_success($appointment_id) { + //if the appointment id doesn't exist or zero redirect to index + if(!$appointment_id){ + redirect('appointments'); + } + $this->load->model('appointments_model'); + $this->load->model('providers_model'); + $this->load->model('services_model'); + $this->load->model('settings_model'); + //retrieve the data needed in the view + $appointment = $this->appointments_model->get_row($appointment_id); + $provider = $this->providers_model->get_row($appointment['id_users_provider']); + $service = $this->services_model->get_row($appointment['id_services']); + $company_name = $this->settings_model->get_setting('company_name'); + //get the exceptions + $exceptions = $this->session->flashdata('book_success'); + // :: LOAD THE BOOK SUCCESS VIEW + $view = array( + 'appointment_data' => $appointment, + 'provider_data' => $provider, + 'service_data' => $service, + 'company_name' => $company_name, + ); + if($exceptions){ + $view['exceptions'] = $exceptions; + } + $this->load->view('appointments/book_success', $view); + } + /** * [AJAX] Get the available appointment hours for the given date. * @@ -316,223 +350,6 @@ class Appointments extends CI_Controller { } } - /** - * Check whether the provider is still available in the selected appointment date. - * - * It might be times where two or more customers select the same appointment date and time. - * This shouldn't be allowed to happen, so one of the two customers will eventually get the - * prefered date and the other one will have to choose for another date. Use this method - * just before the customer confirms the appointment details. If the selected date was taken - * in the mean time, the customer must be prompted to select another time for his appointment. - * - * @param int $_POST['id_users_provider'] The selected provider's record id. - * @param int $_POST['id_services'] The selected service's record id. - * @param string $_POST['start_datetime'] This is a mysql formed string. - * @return bool Returns whether the selected datetime is still available. - */ - public function ajax_check_datetime_availability() { - try { - $this->load->model('services_model'); - - $service_duration = $this->services_model->get_value('duration', $_POST['id_services']); - - $exclude_appointments = (isset($_POST['exclude_appointment_id'])) - ? array($_POST['exclude_appointment_id']) : array(); - - $available_periods = $this->get_provider_available_time_periods( - $_POST['id_users_provider'], $_POST['start_datetime'], $exclude_appointments); - - $is_still_available = FALSE; - - foreach($available_periods as $period) { - $appt_start = new DateTime($_POST['start_datetime']); - $appt_start = $appt_start->format('H:i'); - - $appt_end = new DateTime($_POST['start_datetime']); - $appt_end->add(new DateInterval('PT' . $service_duration . 'M')); - $appt_end = $appt_end->format('H:i'); - - $period_start = date('H:i', strtotime($period['start'])); - $period_end = date('H:i', strtotime($period['end'])); - - if ($period_start <= $appt_start && $period_end >= $appt_end) { - $is_still_available = TRUE; - break; - } - } - - echo json_encode($is_still_available); - - } catch(Exception $exc) { - echo json_encode(array( - 'exceptions' => array(exceptionToJavaScript($exc)) - )); - } - } - - /** - * Get an array containing the free time periods (start - end) of a selected date. - * - * This method is very important because there are many cases where the system needs to - * know when a provider is avaible for an appointment. This method will return an array - * that belongs to the selected date and contains values that have the start and the end - * time of an available time period. - * - * @param numeric $provider_id The provider's record id. - * @param string $selected_date The date to be checked (MySQL formatted string). - * @param array $exclude_appointments This array contains the ids of the appointments that - * will not be taken into consideration when the available time periods are calculated. - * @return array Returns an array with the available time periods of the provider. - */ - private function get_provider_available_time_periods($provider_id, $selected_date, - $exclude_appointments = array()) { - $this->load->model('appointments_model'); - $this->load->model('providers_model'); - - // Get the provider's working plan and reserved appointments. - $working_plan = json_decode($this->providers_model->get_setting('working_plan', $provider_id), true); - - $where_clause = array( - //'DATE(start_datetime)' => date('Y-m-d', strtotime($selected_date)), - 'id_users_provider' => $provider_id - ); - - $reserved_appointments = $this->appointments_model->get_batch($where_clause); - - // Sometimes it might be necessary to not take into account some appointment records - // in order to display what the providers' available time periods would be without them. - foreach ($exclude_appointments as $excluded_id) { - foreach ($reserved_appointments as $index => $reserved) { - if ($reserved['id'] == $excluded_id) { - unset($reserved_appointments[$index]); - } - } - } - - // Find the empty spaces on the plan. The first split between the plan is due to - // a break (if exist). After that every reserved appointment is considered to be - // a taken space in the plan. - $selected_date_working_plan = $working_plan[strtolower(date('l', strtotime($selected_date)))]; - $available_periods_with_breaks = array(); - - if (isset($selected_date_working_plan['breaks'])) { - if (count($selected_date_working_plan['breaks'])) { - foreach($selected_date_working_plan['breaks'] as $index=>$break) { - // Split the working plan to available time periods that do not - // contain the breaks in them. - $last_break_index = $index - 1; - - if (count($available_periods_with_breaks) === 0) { - $start_hour = $selected_date_working_plan['start']; - $end_hour = $break['start']; - } else { - $start_hour = $selected_date_working_plan['breaks'][$last_break_index]['end']; - $end_hour = $break['start']; - } - - $available_periods_with_breaks[] = array( - 'start' => $start_hour, - 'end' => $end_hour - ); - } - - // Add the period from the last break to the end of the day. - $available_periods_with_breaks[] = array( - 'start' => $selected_date_working_plan['breaks'][$index]['end'], - 'end' => $selected_date_working_plan['end'] - ); - } else { - $available_periods_with_breaks[] = array( - 'start' => $selected_date_working_plan['start'], - 'end' => $selected_date_working_plan['end'] - ); - } - } - - // Break the empty periods with the reserved appointments. - $available_periods_with_appointments = $available_periods_with_breaks; - - foreach($reserved_appointments as $appointment) { - foreach($available_periods_with_appointments as $index => &$period) { - - $a_start = strtotime($appointment['start_datetime']); - $a_end = strtotime($appointment['end_datetime']); - $p_start = strtotime($selected_date . ' ' . $period['start']); - $p_end = strtotime($selected_date . ' ' .$period['end']); - - if ($a_start <= $p_start && $a_end <= $p_end && $a_end <= $p_start) { - // The appointment does not belong in this time period, so we - // will not change anything. - } else if ($a_start <= $p_start && $a_end <= $p_end && $a_end >= $p_start) { - // The appointment starts before the period and finishes somewhere inside. - // We will need to break this period and leave the available part. - $period['start'] = date('H:i', $a_end); - - } else if ($a_start >= $p_start && $a_end <= $p_end) { - // The appointment is inside the time period, so we will split the period - // into two new others. - unset($available_periods_with_appointments[$index]); - $available_periods_with_appointments[] = array( - 'start' => date('H:i', $p_start), - 'end' => date('H:i', $a_start) - ); - $available_periods_with_appointments[] = array( - 'start' => date('H:i', $a_end), - 'end' => date('H:i', $p_end) - ); - - } else if ($a_start >= $p_start && $a_end >= $p_start && $a_start <= $p_end) { - // The appointment starts in the period and finishes out of it. We will - // need to remove the time that is taken from the appointment. - $period['end'] = date('H:i', $a_start); - - } else if ($a_start >= $p_start && $a_end >= $p_end && $a_start >= $p_end) { - // The appointment does not belong in the period so do not change anything. - } else if ($a_start <= $p_start && $a_end >= $p_end && $a_start <= $p_end) { - // The appointment is bigger than the period, so this period needs to be - // removed. - unset($available_periods_with_appointments[$index]); - } - } - } - - return array_values($available_periods_with_appointments); - } - - /** - * GET an specific appointment book and redirect to the success screen. - * - * @param int $appointment_id Contains the id of the appointment to retrieve. - */ - public function book_success($appointment_id) { - //if the appointment id doesn't exist or zero redirect to index - if(!$appointment_id){ - redirect('appointments'); - } - $this->load->model('appointments_model'); - $this->load->model('providers_model'); - $this->load->model('services_model'); - $this->load->model('settings_model'); - //retrieve the data needed in the view - $appointment = $this->appointments_model->get_row($appointment_id); - $provider = $this->providers_model->get_row($appointment['id_users_provider']); - $service = $this->services_model->get_row($appointment['id_services']); - $company_name = $this->settings_model->get_setting('company_name'); - //get the exceptions - $exceptions = $this->session->flashdata('book_success'); - // :: LOAD THE BOOK SUCCESS VIEW - $view = array( - 'appointment_data' => $appointment, - 'provider_data' => $provider, - 'service_data' => $service, - 'company_name' => $company_name, - ); - if($exceptions){ - $view['exceptions'] = $exceptions; - } - $this->load->view('appointments/book_success', $view); - } - /** * [AJAX] Register the appointment to the database. */ @@ -546,6 +363,11 @@ class Appointments extends CI_Controller { throw new Exception($this->lang->line('captcha_is_wrong')); } + // Check appointment availability. + if (!$this->check_datetime_availability()) { + throw new Exception($this->lang->line('requested_hour_is_unavailable')); + } + $appointment = $post_data['appointment']; $customer = $post_data['customer']; @@ -660,6 +482,183 @@ class Appointments extends CI_Controller { )); } } + + /** + * Check whether the provider is still available in the selected appointment date. + * + * It might be times where two or more customers select the same appointment date and time. + * This shouldn't be allowed to happen, so one of the two customers will eventually get the + * prefered date and the other one will have to choose for another date. Use this method + * just before the customer confirms the appointment details. If the selected date was taken + * in the mean time, the customer must be prompted to select another time for his appointment. + * + * @return bool Returns whether the selected datetime is still available. + */ + private function check_datetime_availability() { + $this->load->model('services_model'); + + $appointment = $_POST['post_data']['appointment']; + + $service_duration = $this->services_model->get_value('duration', $appointment['id_services']); + + $exclude_appointments = (isset($appointment['appointment_id'])) + ? array($appointment['appointment_id']) : array(); + + $available_periods = $this->get_provider_available_time_periods( + $appointment['id_users_provider'], date('Y-m-d', strtotime($appointment['start_datetime'])), + $exclude_appointments); + + $is_still_available = FALSE; + + foreach($available_periods as $period) { + $appt_start = new DateTime($appointment['start_datetime']); + $appt_start = $appt_start->format('H:i'); + + $appt_end = new DateTime($appointment['start_datetime']); + $appt_end->add(new DateInterval('PT' . $service_duration . 'M')); + $appt_end = $appt_end->format('H:i'); + + $period_start = date('H:i', strtotime($period['start'])); + $period_end = date('H:i', strtotime($period['end'])); + + if ($period_start <= $appt_start && $period_end >= $appt_end) { + $is_still_available = TRUE; + break; + } + } + + return $is_still_available; + } + + /** + * Get an array containing the free time periods (start - end) of a selected date. + * + * This method is very important because there are many cases where the system needs to + * know when a provider is avaible for an appointment. This method will return an array + * that belongs to the selected date and contains values that have the start and the end + * time of an available time period. + * + * @param numeric $provider_id The provider's record id. + * @param string $selected_date The date to be checked (MySQL formatted string). + * @param array $exclude_appointments This array contains the ids of the appointments that + * will not be taken into consideration when the available time periods are calculated. + * @return array Returns an array with the available time periods of the provider. + */ + private function get_provider_available_time_periods($provider_id, $selected_date, + $exclude_appointments = array()) { + $this->load->model('appointments_model'); + $this->load->model('providers_model'); + + // Get the provider's working plan and reserved appointments. + $working_plan = json_decode($this->providers_model + ->get_setting('working_plan', $provider_id), true); + + $where_clause = array( + 'DATE(start_datetime)' => date('Y-m-d', strtotime($selected_date)), + 'id_users_provider' => $provider_id + ); + + $reserved_appointments = $this->appointments_model->get_batch($where_clause); + + // Sometimes it might be necessary to not take into account some appointment records + // in order to display what the providers' available time periods would be without them. + foreach ($exclude_appointments as $excluded_id) { + foreach ($reserved_appointments as $index => $reserved) { + if ($reserved['id'] == $excluded_id) { + unset($reserved_appointments[$index]); + } + } + } + + // Find the empty spaces on the plan. The first split between the plan is due to + // a break (if exist). After that every reserved appointment is considered to be + // a taken space in the plan. + $selected_date_working_plan = $working_plan[strtolower(date('l', strtotime($selected_date)))]; + $available_periods_with_breaks = array(); + + if (isset($selected_date_working_plan['breaks'])) { + if (count($selected_date_working_plan['breaks'])) { + foreach($selected_date_working_plan['breaks'] as $index=>$break) { + // Split the working plan to available time periods that do not + // contain the breaks in them. + $last_break_index = $index - 1; + + if (count($available_periods_with_breaks) === 0) { + $start_hour = $selected_date_working_plan['start']; + $end_hour = $break['start']; + } else { + $start_hour = $selected_date_working_plan['breaks'][$last_break_index]['end']; + $end_hour = $break['start']; + } + + $available_periods_with_breaks[] = array( + 'start' => $start_hour, + 'end' => $end_hour + ); + } + + // Add the period from the last break to the end of the day. + $available_periods_with_breaks[] = array( + 'start' => $selected_date_working_plan['breaks'][$index]['end'], + 'end' => $selected_date_working_plan['end'] + ); + } else { + $available_periods_with_breaks[] = array( + 'start' => $selected_date_working_plan['start'], + 'end' => $selected_date_working_plan['end'] + ); + } + } + + // Break the empty periods with the reserved appointments. + $available_periods_with_appointments = $available_periods_with_breaks; + + foreach($reserved_appointments as $appointment) { + foreach($available_periods_with_appointments as $index => &$period) { + + $a_start = strtotime($appointment['start_datetime']); + $a_end = strtotime($appointment['end_datetime']); + $p_start = strtotime($selected_date . ' ' . $period['start']); + $p_end = strtotime($selected_date . ' ' .$period['end']); + + if ($a_start <= $p_start && $a_end <= $p_end && $a_end <= $p_start) { + // The appointment does not belong in this time period, so we + // will not change anything. + } else if ($a_start <= $p_start && $a_end <= $p_end && $a_end >= $p_start) { + // The appointment starts before the period and finishes somewhere inside. + // We will need to break this period and leave the available part. + $period['start'] = date('H:i', $a_end); + + } else if ($a_start >= $p_start && $a_end <= $p_end) { + // The appointment is inside the time period, so we will split the period + // into two new others. + unset($available_periods_with_appointments[$index]); + $available_periods_with_appointments[] = array( + 'start' => date('H:i', $p_start), + 'end' => date('H:i', $a_start) + ); + $available_periods_with_appointments[] = array( + 'start' => date('H:i', $a_end), + 'end' => date('H:i', $p_end) + ); + + } else if ($a_start >= $p_start && $a_end >= $p_start && $a_start <= $p_end) { + // The appointment starts in the period and finishes out of it. We will + // need to remove the time that is taken from the appointment. + $period['end'] = date('H:i', $a_start); + + } else if ($a_start >= $p_start && $a_end >= $p_end && $a_start >= $p_end) { + // The appointment does not belong in the period so do not change anything. + } else if ($a_start <= $p_start && $a_end >= $p_end && $a_start <= $p_end) { + // The appointment is bigger than the period, so this period needs to be + // removed. + unset($available_periods_with_appointments[$index]); + } + } + } + + return array_values($available_periods_with_appointments); + } } /* End of file appointments.php */ diff --git a/src/application/language/english/translations_lang.php b/src/application/language/english/translations_lang.php index 2ae8e00c..86e2e336 100644 --- a/src/application/language/english/translations_lang.php +++ b/src/application/language/english/translations_lang.php @@ -266,3 +266,4 @@ $lang['could_not_add_to_google_calendar'] = 'Your appointment could not be added $lang['ea_update_success'] = 'Easy!Appointments has been successfully updated!'; $lang['captcha_is_wrong'] = 'CAPTCHA verification failed, please try again.'; $lang['any_provider'] = 'Any Provider'; +$lang['requested_hour_is_unavailable'] = 'The requested appointment is unfornately not available. Please select a different hour for your appointment.'; diff --git a/src/assets/js/frontend_book.js b/src/assets/js/frontend_book.js index b38bb252..600478c2 100644 --- a/src/assets/js/frontend_book.js +++ b/src/assets/js/frontend_book.js @@ -261,44 +261,7 @@ var FrontendBook = { * another customer or event. */ $('#book-appointment-submit').click(function(event) { - var formData = jQuery.parseJSON($('input[name="post_data"]').val()); - - var postData = { - 'csrfToken': GlobalVariables.csrfToken, - 'id_users_provider': formData['appointment']['id_users_provider'], - 'id_services': formData['appointment']['id_services'], - 'start_datetime': formData['appointment']['start_datetime'], - }; - - if (GlobalVariables.manageMode) { - postData.exclude_appointment_id = GlobalVariables.appointmentData.id; - } - - var postUrl = GlobalVariables.baseUrl + '/index.php/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 occurred:'); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); - return false; - } - - if (response === true) { - FrontendBook.registerAppointment(); - } 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').fail(GeneralFunctions.ajaxFailureHandler); + FrontendBook.registerAppointment(); }); /** @@ -676,12 +639,7 @@ var FrontendBook = { } }) .done(function(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 occurred:'); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + if (!GeneralFunctions.handleAjaxExceptions(response)) { $('.captcha-title small').trigger('click'); return false; }