diff --git a/application/config/config.php b/application/config/config.php index 5a659a3d..96df7658 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -9,7 +9,7 @@ | */ $config['version'] = '1.4.0'; // This must be changed manually. -$config['release_label'] = 'Dev'; // Leave empty for no title or add Alpha, Beta etc ... +$config['release_label'] = 'Beta.1'; // Leave empty for no title or add Alpha, Beta etc ... $config['debug'] = Config::DEBUG_MODE; /* @@ -302,7 +302,7 @@ $config['cache_path'] = __DIR__ . '/../../storage/cache/'; | new release. | */ -$config['cache_busting_token'] = '93GX4'; +$config['cache_busting_token'] = '824HX'; /* |-------------------------------------------------------------------------- diff --git a/application/controllers/Appointments.php b/application/controllers/Appointments.php index 2276cf05..5a6c117d 100755 --- a/application/controllers/Appointments.php +++ b/application/controllers/Appointments.php @@ -11,11 +11,6 @@ * @since v1.0.0 * ---------------------------------------------------------------------------- */ -use EA\Engine\Notifications\Email as EmailClient; -use EA\Engine\Types\Email; -use EA\Engine\Types\Text; -use EA\Engine\Types\Url; - /** * Appointments Controller * @@ -35,11 +30,14 @@ use EA\Engine\Types\Url; * @property Services_Model $services_model * @property Customers_Model $customers_model * @property Settings_Model $settings_model - * @property Timezones $timezones * @property Roles_Model $roles_model * @property Secretaries_Model $secretaries_model * @property Admins_Model $admins_model * @property User_Model $user_model + * @property Timezones $timezones + * @property Synchronization $synchronization + * @property Notifications $notifications + * @property Availability $availability * * @package Controllers */ @@ -51,7 +49,6 @@ class Appointments extends CI_Controller { { parent::__construct(); - $this->load->library('session'); $this->load->helper('installation'); $this->load->helper('google_analytics'); $this->load->model('appointments_model'); @@ -61,7 +58,11 @@ class Appointments extends CI_Controller { $this->load->model('services_model'); $this->load->model('customers_model'); $this->load->model('settings_model'); + $this->load->library('session'); $this->load->library('timezones'); + $this->load->library('synchronization'); + $this->load->library('notifications'); + $this->load->library('availability'); if ($this->session->userdata('language')) { @@ -147,8 +148,8 @@ class Appointments extends CI_Controller { return; } - // If the requested apppointment begin date is lower than book_advance_timeout. Display - // a message to the customer. + // If the requested appointment begin date is lower than book_advance_timeout. Display a message to the + // customer. $startDate = strtotime($results[0]['start_datetime']); $limit = strtotime('+' . $book_advance_timeout . ' minutes', strtotime('now')); @@ -175,6 +176,7 @@ class Appointments extends CI_Controller { $customer_token = md5(uniqid(mt_rand(), TRUE)); $this->load->driver('cache', ['adapter' => 'file']); + // Save the token for 10 minutes. $this->cache->save('customer-token-' . $customer_token, $customer['id'], 600); } @@ -261,96 +263,8 @@ class Appointments extends CI_Controller { throw new Exception('Appointment could not be deleted from the database.'); } - // Remove the appointment from Google Calendar if needed. - if ($appointment['id_google_calendar'] != NULL) - { - try - { - $google_sync = filter_var( - $this->providers_model->get_setting('google_sync', $appointment['id_users_provider']), - FILTER_VALIDATE_BOOLEAN); - - if ($google_sync === TRUE) - { - $google_token = json_decode($this->providers_model - ->get_setting('google_token', $provider['id'])); - $this->load->library('Google_sync'); - $this->google_sync->refresh_token($google_token->refresh_token); - $this->google_sync->delete_appointment($provider, $appointment['id_google_calendar']); - } - } - catch (Exception $exception) - { - $exceptions[] = $exception; - } - } - - // Send email notification to customer and provider. - try - { - $email = new EmailClient($this, $this->config->config); - - $send_provider = filter_var($this->providers_model - ->get_setting('notifications', $provider['id']), - FILTER_VALIDATE_BOOLEAN); - - if ($send_provider === TRUE) - { - $email->sendDeleteAppointment($appointment, $provider, - $service, $customer, $settings, new Email($provider['email']), - new Text($this->input->post('cancel_reason'))); - } - - $send_customer = filter_var( - $this->settings_model->get_setting('customer_notifications'), - FILTER_VALIDATE_BOOLEAN); - - if ($send_customer === TRUE) - { - $email->sendDeleteAppointment($appointment, $provider, - $service, $customer, $settings, new Email($customer['email']), - new Text($this->input->post('cancel_reason'))); - } - - // Notify admins - $admins = $this->admins_model->get_batch(); - - foreach($admins as $admin) - { - if (!$admin['settings']['notifications'] === '0') - { - continue; - } - - $email->sendDeleteAppointment($appointment, $provider, - $service, $customer, $settings, new Email($admin['email']), - new Text($this->input->post('cancel_reason'))); - } - - // Notify secretaries - $secretaries = $this->secretaries_model->get_batch(); - - foreach($secretaries as $secretary) - { - if (!$secretary['settings']['notifications'] === '0') - { - continue; - } - - if (in_array($provider['id'], $secretary['providers'])) - { - continue; - } - - $email->sendDeleteAppointment($appointment, $provider, - $service, $customer, $settings, new Email($secretary['email']), - new Text($this->input->post('cancel_reason'))); - } - } - catch (Exception $exception) - { - $exceptions[] = $exception; - } + $this->synchronization->sync_appointment_deleted($appointment, $provider); + $this->notifications->notify_appointment_deleted($appointment, $service, $provider, $customer, $settings); } catch (Exception $exception) { @@ -445,12 +359,7 @@ class Appointments extends CI_Controller { // If manage mode is TRUE then the following we should not consider the selected appointment when // calculating the available time periods of the provider. - $exclude_appointments = []; - - if ($this->input->post('manage_mode') === 'true') - { - $exclude_appointments[] = $this->input->post('appointment_id'); - } + $exclude_appointment_id = $this->input->post('manage_mode') === 'true' ? $this->input->post('appointment_id') : NULL; // If the user has selected the "any-provider" option then we will need to search for an available provider // that will provide the requested service. @@ -469,21 +378,10 @@ class Appointments extends CI_Controller { } $service = $this->services_model->get_row($service_id); + $provider = $this->providers_model->get_row($provider_id); - $empty_periods = $this->get_provider_available_time_periods($provider_id, - $selected_date, $exclude_appointments); - - $available_hours = $this->calculate_available_hours($empty_periods, $selected_date, - $service['duration'], $service['availabilities_type']); - - if ($service['attendants_number'] > 1) - { - $available_hours = $this->get_multiple_attendants_hours($selected_date, $service, - $provider); - } - - $response = $this->consider_book_advance_timeout($selected_date, $available_hours, $provider); + $response = $this->availability->get_available_hours($selected_date, $service, $provider, $exclude_appointment_id); } catch (Exception $exception) { @@ -505,14 +403,14 @@ class Appointments extends CI_Controller { * * This method will return the database ID of the provider with the most available periods. * + * @param string $date The date to be searched (Y-m-d). * @param int $service_id The requested service ID. - * @param string $selected_date The date to be searched. * * @return int Returns the ID of the provider that can provide the service at the selected date. * * @throws Exception */ - protected function search_any_provider($service_id, $selected_date) + protected function search_any_provider($date, $service_id) { $available_providers = $this->providers_model->get_available_providers(); @@ -529,16 +427,7 @@ class Appointments extends CI_Controller { if ($provider_service_id == $service_id) { // Check if the provider is available for the requested date. - $empty_periods = $this->get_provider_available_time_periods($provider['id'], $selected_date); - - $available_hours = $this->calculate_available_hours($empty_periods, $selected_date, - $service['duration'], $service['availabilities_type']); - - if ($service['attendants_number'] > 1) - { - $available_hours = $this->get_multiple_attendants_hours($selected_date, $service, - $provider); - } + $available_hours = $this->availability->get_available_hours($date, $service, $provider); if (count($available_hours) > $max_hours_count) { @@ -552,454 +441,6 @@ class Appointments extends CI_Controller { return $provider_id; } - /** - * 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 - * available 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 int $provider_id Provider record ID. - * @param string $selected_date Date to be checked (MySQL formatted string). - * @param array $excluded_appointment_ids Array containing 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. - * - * @throws Exception - */ - protected function get_provider_available_time_periods( - $provider_id, - $selected_date, - $excluded_appointment_ids = [] - ) - { - // Get the service, provider's working plan and provider appointments. - $working_plan = json_decode($this->providers_model->get_setting('working_plan', $provider_id), TRUE); - - // Get the provider's working plan exceptions. - $working_plan_exceptions = json_decode($this->providers_model->get_setting('working_plan_exceptions', $provider_id), TRUE); - - $provider_appointments = $this->appointments_model->get_batch([ - 'id_users_provider' => $provider_id, - ]); - - // 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 ($excluded_appointment_ids as $excluded_appointment_id) - { - foreach ($provider_appointments as $index => $reserved) - { - if ($reserved['id'] == $excluded_appointment_id) - { - unset($provider_appointments[$index]); - } - } - } - - // Find the empty spaces on the plan. The first split between the plan is due to a break (if any). 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)))]; - - // Search if the $selected_date is an custom availability period added outside the normal working plan. - if (isset($working_plan_exceptions[$selected_date])) - { - $selected_date_working_plan = $working_plan_exceptions[$selected_date]; - } - - $periods = []; - - if (isset($selected_date_working_plan['breaks'])) - { - $periods[] = [ - 'start' => $selected_date_working_plan['start'], - 'end' => $selected_date_working_plan['end'] - ]; - - $day_start = new DateTime($selected_date_working_plan['start']); - $day_end = new DateTime($selected_date_working_plan['end']); - - // Split the working plan to available time periods that do not contain the breaks in them. - foreach ($selected_date_working_plan['breaks'] as $index => $break) - { - $break_start = new DateTime($break['start']); - $break_end = new DateTime($break['end']); - - if ($break_start < $day_start) - { - $break_start = $day_start; - } - - if ($break_end > $day_end) - { - $break_end = $day_end; - } - - if ($break_start >= $break_end) - { - continue; - } - - foreach ($periods as $key => $period) - { - $period_start = new DateTime($period['start']); - $period_end = new DateTime($period['end']); - - $remove_current_period = FALSE; - - if ($break_start > $period_start && $break_start < $period_end && $break_end > $period_start) - { - $periods[] = [ - 'start' => $period_start->format('H:i'), - 'end' => $break_start->format('H:i') - ]; - - $remove_current_period = TRUE; - } - - if ($break_start < $period_end && $break_end > $period_start && $break_end < $period_end) - { - $periods[] = [ - 'start' => $break_end->format('H:i'), - 'end' => $period_end->format('H:i') - ]; - - $remove_current_period = TRUE; - } - - if ($break_start == $period_start && $break_end == $period_end) - { - $remove_current_period = TRUE; - } - - if ($remove_current_period) - { - unset($periods[$key]); - } - } - } - } - - // Break the empty periods with the reserved appointments. - foreach ($provider_appointments as $provider_appointment) - { - foreach ($periods as $index => &$period) - { - $appointment_start = new DateTime($provider_appointment['start_datetime']); - $appointment_end = new DateTime($provider_appointment['end_datetime']); - - if ($appointment_start >= $appointment_end) - { - continue; - } - - $period_start = new DateTime($selected_date . ' ' . $period['start']); - $period_end = new DateTime($selected_date . ' ' . $period['end']); - - if ($appointment_start <= $period_start && $appointment_end <= $period_end && $appointment_end <= $period_start) - { - // The appointment does not belong in this time period, so we will not change anything. - continue; - } - else - { - if ($appointment_start <= $period_start && $appointment_end <= $period_end && $appointment_end >= $period_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'] = $appointment_end->format('H:i'); - } - else - { - if ($appointment_start >= $period_start && $appointment_end < $period_end) - { - // The appointment is inside the time period, so we will split the period into two new - // others. - unset($periods[$index]); - - $periods[] = [ - 'start' => $period_start->format('H:i'), - 'end' => $appointment_start->format('H:i') - ]; - - $periods[] = [ - 'start' => $appointment_end->format('H:i'), - 'end' => $period_end->format('H:i') - ]; - } - else if ($appointment_start == $period_start && $appointment_end == $period_end) - { - unset($periods[$index]); // The whole period is blocked so remove it from the available periods array. - } - else - { - if ($appointment_start >= $period_start && $appointment_end >= $period_start && $appointment_start <= $period_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'] = $appointment_start->format('H:i'); - } - else - { - if ($appointment_start >= $period_start && $appointment_end >= $period_end && $appointment_start >= $period_end) - { - // The appointment does not belong in the period so do not change anything. - continue; - } - else - { - if ($appointment_start <= $period_start && $appointment_end >= $period_end && $appointment_start <= $period_end) - { - // The appointment is bigger than the period, so this period needs to be removed. - unset($periods[$index]); - } - } - } - } - } - } - } - } - - return array_values($periods); - } - - /** - * Calculate the available appointment hours. - * - * Calculate the available appointment hours for the given date. The empty spaces - * are broken down to 15 min and if the service fit in each quarter then a new - * available hour is added to the "$available_hours" array. - * - * @param array $empty_periods Contains the empty periods as generated by the "get_provider_available_time_periods" - * method. - * @param string $selected_date The selected date to be search (format ) - * @param int $service_duration The service duration is required for the hour calculation. - * @param string $availabilities_type Optional ('flexible'), the service availabilities type. - * - * @return array Returns an array with the available hours for the appointment. - * @throws Exception - */ - protected function calculate_available_hours( - array $empty_periods, - $selected_date, - $service_duration, - $availabilities_type = 'flexible' - ) - { - $available_hours = []; - - foreach ($empty_periods as $period) - { - $start_hour = new DateTime($selected_date . ' ' . $period['start']); - $end_hour = new DateTime($selected_date . ' ' . $period['end']); - $interval = $availabilities_type === AVAILABILITIES_TYPE_FIXED ? (int)$service_duration : 15; - - $current_hour = $start_hour; - $diff = $current_hour->diff($end_hour); - - while (($diff->h * 60 + $diff->i) >= intval($service_duration)) - { - $available_hours[] = $current_hour->format('H:i'); - $current_hour->add(new DateInterval('PT' . $interval . 'M')); - $diff = $current_hour->diff($end_hour); - } - } - - return $available_hours; - } - - /** - * Get multiple attendants hours. - * - * This method will add the additional appointment hours whenever a service accepts multiple attendants. - * - * @param string $selected_date The selected appointment date. - * @param array $service Selected service data. - * @param array $provider Selected provider data. - * - * @return array Returns the available hours array. - * @throws Exception - */ - protected function get_multiple_attendants_hours( - $selected_date, - $service, - $provider - ) - { - $unavailability_events = $this->appointments_model->get_batch([ - 'is_unavailable' => TRUE, - 'DATE(start_datetime)' => $selected_date, - 'id_users_provider' => $provider['id'] - ]); - - $working_plan = json_decode($provider['settings']['working_plan'], TRUE); - $working_day = strtolower(date('l', strtotime($selected_date))); - $working_hours = $working_plan[$working_day]; - - $periods = [ - [ - 'start' => new DateTime($selected_date . ' ' . $working_hours['start']), - 'end' => new DateTime($selected_date . ' ' . $working_hours['end']) - ] - ]; - - $periods = $this->remove_breaks($selected_date, $periods, $working_hours['breaks']); - $periods = $this->remove_unavailability_events($periods, $unavailability_events); - - $hours = []; - - $interval_value = $service['availabilities_type'] == AVAILABILITIES_TYPE_FIXED ? $service['duration'] : '15'; - $interval = new DateInterval('PT' . (int)$interval_value . 'M'); - $duration = new DateInterval('PT' . (int)$service['duration'] . 'M'); - - foreach ($periods as $period) - { - $slot_start = clone $period['start']; - $slot_end = clone $slot_start; - $slot_end->add($duration); - - while ($slot_end <= $period['end']) - { - // Check reserved attendants for this time slot and see if current attendants fit. - $appointment_attendants_number = $this->appointments_model->get_attendants_number_for_period($slot_start, - $slot_end, $service['id']); - - if ($appointment_attendants_number < $service['attendants_number']) - { - $hours[] = $slot_start->format('H:i'); - } - - $slot_start->add($interval); - $slot_end->add($interval); - } - } - - return $hours; - } - - /** - * Remove breaks from available time periods. - * - * @param string $selected_date Selected data (Y-m-d format). - * @param array $periods Time periods of the current date. - * @param array $breaks Breaks array for the current date. - * - * @return array Returns the available time periods without the breaks. - * @throws Exception - */ - public function remove_breaks($selected_date, $periods, $breaks) - { - if ( ! $breaks) - { - return $periods; - } - - foreach ($breaks as $break) - { - $break_start = new DateTime($selected_date . ' ' . $break['start']); - $break_end = new DateTime($selected_date . ' ' . $break['end']); - - foreach ($periods as &$period) - { - $period_start = $period['start']; - $period_end = $period['end']; - - if ($break_start <= $period_start && $break_end >= $period_start && $break_end <= $period_end) - { - // left - $period['start'] = $break_end; - continue; - } - - if ($break_start >= $period_start && $break_start <= $period_end && $break_end >= $period_start && $break_end <= $period_end) - { - // middle - $period['end'] = $break_start; - $periods[] = [ - 'start' => $break_end, - 'end' => $period_end - ]; - continue; - } - - if ($break_start >= $period_start && $break_start <= $period_end && $break_end >= $period_end) - { - // right - $period['end'] = $break_start; - continue; - } - - if ($break_start <= $period_start && $break_end >= $period_end) - { - // break contains period - $period['start'] = $break_end; - continue; - } - } - } - - return $periods; - } - - /** - * Remove the unavailability entries from the available time periods of the selected date. - * - * @param array $periods Available time periods. - * @param array $unavailability_events Unavailability events of the current date. - * - * @return array Returns the available time periods without the unavailability events. - * - * @throws Exception - */ - public function remove_unavailability_events($periods, $unavailability_events) - { - foreach ($unavailability_events as $unavailability_event) - { - $unavailability_start = new DateTime($unavailability_event['start_datetime']); - $unavailability_end = new DateTime($unavailability_event['end_datetime']); - - foreach ($periods as &$period) - { - $period_start = $period['start']; - $period_end = $period['end']; - - if ($unavailability_start <= $period_start && $unavailability_end >= $period_start && $unavailability_end <= $period_end) - { - // left - $period['start'] = $unavailability_end; - continue; - } - - if ($unavailability_start >= $period_start && $unavailability_start <= $period_end && $unavailability_end >= $period_start && $unavailability_end <= $period_end) - { - // middle - $period['end'] = $unavailability_start; - $periods[] = [ - 'start' => $unavailability_end, - 'end' => $period_end - ]; - continue; - } - - if ($unavailability_start >= $period_start && $unavailability_start <= $period_end && $unavailability_end >= $period_end) - { - // right - $period['end'] = $unavailability_start; - continue; - } - - if ($unavailability_start <= $period_start && $unavailability_end >= $period_end) - { - // Unavailability contains period - $period['start'] = $unavailability_end; - continue; - } - } - } - - return $periods; - } /** * [AJAX] Register the appointment to the database. @@ -1017,6 +458,8 @@ class Appointments extends CI_Controller { $this->load->model('services_model'); $this->load->model('customers_model'); $this->load->model('settings_model'); + $this->load->library('notifications'); + $this->load->library('synchronization'); $post_data = $this->input->post('post_data'); $captcha = $this->input->post('captcha'); @@ -1077,139 +520,8 @@ class Appointments extends CI_Controller { 'time_format' => $this->settings_model->get_setting('time_format') ]; - // Synchronize the appointment with the provider's Google Calendar. - try - { - $google_sync = filter_var( - $this->providers_model->get_setting('google_sync', $appointment['id_users_provider']), - FILTER_VALIDATE_BOOLEAN); - - if ($google_sync === TRUE) - { - $google_token = json_decode( - $this->providers_model->get_setting('google_token', $appointment['id_users_provider'])); - - $this->load->library('google_sync'); - - $this->google_sync->refresh_token($google_token->refresh_token); - - if ($manage_mode === FALSE) - { - // Add appointment to Google Calendar. - $google_event = $this->google_sync->add_appointment($appointment, $provider, - $service, $customer, $settings); - $appointment['id_google_calendar'] = $google_event->id; - $this->appointments_model->add($appointment); - } - else - { - // Update appointment to Google Calendar. - $appointment['id_google_calendar'] = $this->appointments_model - ->get_value('id_google_calendar', $appointment['id']); - - $this->google_sync->update_appointment($appointment, $provider, - $service, $customer, $settings); - } - } - } - catch (Exception $exception) - { - log_message('error', $exception->getMessage()); - log_message('error', $exception->getTraceAsString()); - } - - // Send email notifications to customer and provider. - try - { - $this->config->load('email'); - - $email = new EmailClient($this, $this->config->config); - - if ($manage_mode === FALSE) - { - $customer_title = new Text(lang('appointment_booked')); - $customer_message = new Text(lang('thank_you_for_appointment')); - $provider_title = new Text(lang('appointment_added_to_your_plan')); - $provider_message = new Text(lang('appointment_link_description')); - - } - else - { - $customer_title = new Text(lang('appointment_changes_saved')); - $customer_message = new Text(''); - $provider_title = new Text(lang('appointment_details_changed')); - $provider_message = new Text(''); - } - - $customer_link = new Url(site_url('appointments/index/' . $appointment['hash'])); - $provider_link = new Url(site_url('backend/index/' . $appointment['hash'])); - - $send_customer = filter_var( - $this->settings_model->get_setting('customer_notifications'), - FILTER_VALIDATE_BOOLEAN); - - $this->load->library('ics_file'); - - $ics_stream = $this->ics_file->get_stream($appointment, $service, $provider, $customer); - - if ($send_customer === TRUE) - { - $email->sendAppointmentDetails($appointment, $provider, - $service, $customer, $settings, $customer_title, - $customer_message, $customer_link, new Email($customer['email']), new Text($ics_stream)); - } - - $send_provider = filter_var( - $this->providers_model->get_setting('notifications', $provider['id']), - FILTER_VALIDATE_BOOLEAN); - - if ($send_provider === TRUE) - { - $email->sendAppointmentDetails($appointment, $provider, - $service, $customer, $settings, $provider_title, - $provider_message, $provider_link, new Email($provider['email']), new Text($ics_stream)); - } - - // Notify admins - $admins = $this->admins_model->get_batch(); - - foreach($admins as $admin) - { - if (!$admin['settings']['notifications'] === '0') - { - continue; - } - - $email->sendAppointmentDetails($appointment, $provider, - $service, $customer, $settings, $provider_title, - $provider_message, $provider_link, new Email($admin['email']), new Text($ics_stream)); - } - - // Notify secretaries - $secretaries = $this->secretaries_model->get_batch(); - - foreach($secretaries as $secretary) - { - if (!$secretary['settings']['notifications'] === '0') - { - continue; - } - - if (in_array($provider['id'], $secretary['providers'])) - { - continue; - } - - $email->sendAppointmentDetails($appointment, $provider, - $service, $customer, $settings, $provider_title, - $provider_message, $provider_link, new Email($secretary['email']), new Text($ics_stream)); - } - } - catch (Exception $exception) - { - log_message('error', $exception->getMessage()); - log_message('error', $exception->getTraceAsString()); - } + $this->synchronization->sync_appointment_saved($appointment, $service, $provider, $customer, $settings); + $this->notifications->notify_appointment_saved($appointment, $service, $provider, $customer, $settings, $manage_mode); $response = [ 'appointment_id' => $appointment['id'], @@ -1246,69 +558,43 @@ class Appointments extends CI_Controller { */ protected function check_datetime_availability() { - $this->load->model('services_model'); - $this->load->model('appointments_model'); - $post_data = $this->input->post('post_data'); $appointment = $post_data['appointment']; - $service_duration = $this->services_model->get_value('duration', $appointment['id_services']); - - $exclude_appointments = []; - - if (isset($appointment['id'])) - { - $exclude_appointments[] = $appointment['id']; - } - - $attendants_number = $this->services_model->get_value('attendants_number', $appointment['id_services']); - - if ($attendants_number > 1) - { - // Exclude all the appointments that are currently registered. - $existing_appointments = $this->appointments_model->get_batch([ - 'id_services' => $appointment['id_services'], - 'start_datetime' => $appointment['start_datetime'] - ]); - - if ( ! empty($existing_appointments) && count($existing_appointments) < $attendants_number) - { - foreach ($existing_appointments as $existing_appointment) - { - $exclude_appointments[] = $existing_appointment['id']; - } - } - } + $date = date('Y-m-d', strtotime($appointment['start_datetime'])); if ($appointment['id_users_provider'] === ANY_PROVIDER) { - $appointment['id_users_provider'] = $this->search_any_provider($appointment['id_services'], - date('Y-m-d', strtotime($appointment['start_datetime']))); + + $appointment['id_users_provider'] = $this->search_any_provider($date, $appointment['id_services']); return $appointment['id_users_provider']; } - $available_periods = $this->get_provider_available_time_periods( - $appointment['id_users_provider'], - date('Y-m-d', strtotime($appointment['start_datetime'])), - $exclude_appointments); + $service = $this->services_model->get_row($appointment['id_services']); + + $exclude_appointment_id = isset($appointment['id']) ? $appointment['id'] : NULL; + + $provider = $this->providers_model->get_row($appointment['id_users_provider']); + + $available_periods = $this->availability->get_available_periods($date, $provider, $exclude_appointment_id); $is_still_available = FALSE; - foreach ($available_periods as $period) + foreach ($available_periods as $available_period) { - $appt_start = new DateTime($appointment['start_datetime']); - $appt_start = $appt_start->format('H:i'); + $appointment_start = new DateTime($appointment['start_datetime']); + $appointment_start = $appointment_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'); + $appointment_end = new DateTime($appointment['start_datetime']); + $appointment_end->add(new DateInterval('PT' . $service['duration'] . 'M')); + $appointment_end = $appointment_end->format('H:i'); - $period_start = date('H:i', strtotime($period['start'])); - $period_end = date('H:i', strtotime($period['end'])); + $available_period_start = date('H:i', strtotime($available_period['start'])); + $available_period_end = date('H:i', strtotime($available_period['end'])); - if ($period_start <= $appt_start && $period_end >= $appt_end) + if ($available_period_start <= $appointment_start && $available_period_end >= $appointment_end) { $is_still_available = TRUE; break; @@ -1343,17 +629,12 @@ class Appointments extends CI_Controller { $number_of_days_in_month = (int)$selected_date->format('t'); $unavailable_dates = []; - $exclude_appointments = []; - - if ($manage_mode === 'true') - { - $exclude_appointments[] = $appointment_id; - } - - $provider_list = $provider_id === ANY_PROVIDER + $provider_ids = $provider_id === ANY_PROVIDER ? $this->search_providers_by_service($service_id) : [$provider_id]; + $exclude_appointment_id = $manage_mode ? $appointment_id : NULL; + // Get the service record. $service = $this->services_model->get_row($service_id); @@ -1369,35 +650,21 @@ class Appointments extends CI_Controller { } // Finding at least one slot of availability. - foreach ($provider_list as $current_provider_id) + foreach ($provider_ids as $current_provider_id) { - // Get the provider record. - $current_provider = $this->providers_model->get_row($current_provider_id); + $provider = $this->providers_model->get_row($current_provider_id); - $empty_periods = $this->get_provider_available_time_periods($current_provider_id, - $current_date->format('Y-m-d'), $exclude_appointments); - - $available_hours = $this->calculate_available_hours($empty_periods, $current_date->format('Y-m-d'), - $service['duration'], $service['availabilities_type']); + $available_hours = $this->availability->get_available_hours( + $current_date->format('Y-m-d'), + $service, + $provider, + $exclude_appointment_id + ); if ( ! empty($available_hours)) { break; } - - if ($service['attendants_number'] > 1) - { - $available_hours = $this->get_multiple_attendants_hours($current_date->format('Y-m-d'), - $service, $current_provider); - - if ( ! empty($available_hours)) - { - break; - } - } - - $available_hours = $this->consider_book_advance_timeout($current_date->format('Y-m-d'), - $available_hours, $current_provider); } // No availability amongst all the provider. @@ -1454,41 +721,5 @@ class Appointments extends CI_Controller { return $provider_list; } - /** - * Consider the book advance timeout and remove available hours that have passed the threshold. - * - * If the selected date is today, remove past hours. It is important include the timeout before booking - * that is set in the back-office the system. Normally we might want the customer to book an appointment - * that is at least half or one hour from now. The setting is stored in minutes. - * - * @param string $selected_date The selected date. - * @param array $available_hours Already generated available hours. - * @param array $provider Provider information. - * - * @return array Returns the updated available hours. - * - * @throws Exception - */ - protected function consider_book_advance_timeout($selected_date, $available_hours, $provider) - { - $provider_timezone = new DateTimeZone($provider['timezone']); - $book_advance_timeout = $this->settings_model->get_setting('book_advance_timeout'); - - $threshold = new DateTime('+' . $book_advance_timeout . ' minutes', $provider_timezone); - - foreach ($available_hours as $index => $value) - { - $available_hour = new DateTime($selected_date . ' ' . $value, $provider_timezone); - - if ($available_hour->getTimestamp() <= $threshold->getTimestamp()) - { - unset($available_hours[$index]); - } - } - - $available_hours = array_values($available_hours); - sort($available_hours, SORT_STRING); - return array_values($available_hours); - } } diff --git a/application/controllers/Backend_api.php b/application/controllers/Backend_api.php index eb34d50f..5a6e7896 100755 --- a/application/controllers/Backend_api.php +++ b/application/controllers/Backend_api.php @@ -38,6 +38,8 @@ use EA\Engine\Types\Url; * @property Customers_Model $customers_model * @property Settings_Model $settings_model * @property Timezones $timezones + * @property Synchronization $synchronization + * @property Notifications $notifications * @property Roles_Model $roles_model * @property Secretaries_Model $secretaries_model * @property Admins_Model $admins_model @@ -102,7 +104,7 @@ class Backend_api extends CI_Controller { 'start_datetime >=' => $startDate, 'end_datetime <=' => $endDate ]), - 'unavailabilities' => $this->appointments_model->get_batch([ + 'unavailability_events' => $this->appointments_model->get_batch([ 'is_unavailable' => TRUE, 'start_datetime >=' => $startDate, 'end_datetime <=' => $endDate @@ -130,11 +132,11 @@ class Backend_api extends CI_Controller { } } - foreach ($response['unavailabilities'] as $index => $unavailability) + foreach ($response['unavailability_events'] as $index => $unavailability_event) { - if ((int)$unavailability['id_users_provider'] !== (int)$userId) + if ((int)$unavailability_event['id_users_provider'] !== (int)$userId) { - unset($response['unavailabilities'][$index]); + unset($response['unavailability_events'][$index]); } } } @@ -152,11 +154,11 @@ class Backend_api extends CI_Controller { } } - foreach ($response['unavailabilities'] as $index => $unavailability) + foreach ($response['unavailability_events'] as $index => $unavailability_event) { - if ( ! in_array((int)$unavailability['id_users_provider'], $providers)) + if ( ! in_array((int)$unavailability_event['id_users_provider'], $providers)) { - unset($response['unavailabilities'][$index]); + unset($response['unavailability_events'][$index]); } } } @@ -283,6 +285,8 @@ class Backend_api extends CI_Controller { $this->load->model('customers_model'); $this->load->model('settings_model'); $this->load->library('timezones'); + $this->load->library('synchronization'); + $this->load->library('notifications'); $this->load->model('user_model'); // Save customer changes to the database. @@ -290,10 +294,10 @@ class Backend_api extends CI_Controller { { $customer = json_decode($this->input->post('customer_data'), TRUE); - $required_privilegesileges = ( ! isset($customer['id'])) + $required_privileges = ( ! isset($customer['id'])) ? $this->privileges[PRIV_CUSTOMERS]['add'] : $this->privileges[PRIV_CUSTOMERS]['edit']; - if ($required_privilegesileges == FALSE) + if ($required_privileges == FALSE) { throw new Exception('You do not have the required privileges for this task.'); } @@ -306,17 +310,17 @@ class Backend_api extends CI_Controller { { $appointment = json_decode($this->input->post('appointment_data'), TRUE); - $required_privilegesileges = ( ! isset($appointment['id'])) + $required_privileges = ( ! isset($appointment['id'])) ? $this->privileges[PRIV_APPOINTMENTS]['add'] : $this->privileges[PRIV_APPOINTMENTS]['edit']; - if ($required_privilegesileges == FALSE) + if ($required_privileges == FALSE) { throw new Exception('You do not have the required privileges for this task.'); } $manage_mode = isset($appointment['id']); - // If the appointment does not contain the customer record id, then it - // means that is is going to be inserted. Get the customer's record id. + // If the appointment does not contain the customer record id, then it means that is is going to be + // inserted. Get the customer's record ID. if ( ! isset($appointment['id_users_customer'])) { $appointment['id_users_customer'] = $customer['id']; @@ -348,139 +352,10 @@ class Backend_api extends CI_Controller { 'time_format' => $this->settings_model->get_setting('time_format') ]; - // Sync appointment changes with Google Calendar. - try - { - $google_sync = $this->providers_model->get_setting('google_sync', - $appointment['id_users_provider']); + $this->synchronization->sync_appointment_deleted($appointment, $provider); + $this->notifications->notify_appointment_deleted($appointment, $service, $provider, $customer, $settings); - if ($google_sync == TRUE) - { - $google_token = json_decode($this->providers_model->get_setting('google_token', - $appointment['id_users_provider'])); - - $this->load->library('Google_sync'); - $this->google_sync->refresh_token($google_token->refresh_token); - - if ($appointment['id_google_calendar'] == NULL) - { - $google_event = $this->google_sync->add_appointment($appointment, $provider, - $service, $customer, $settings); - $appointment['id_google_calendar'] = $google_event->id; - $this->appointments_model->add($appointment); // Store google calendar id. - } - else - { - $this->google_sync->update_appointment($appointment, $provider, - $service, $customer, $settings); - } - } - } - catch (Exception $exception) - { - $warnings[] = [ - 'message' => $exception->getMessage(), - 'trace' => config('debug') ? $exception->getTrace() : [] - ]; - } - - // Send email notifications to provider and customer. - try - { - $this->config->load('email'); - $email = new EmailClient($this, $this->config->config); - - $send_provider = $this->providers_model - ->get_setting('notifications', $provider['id']); - - if ( ! $manage_mode) - { - $customer_title = new Text(lang('appointment_booked')); - $customer_message = new Text(lang('thank_you_for_appointment')); - $provider_title = new Text(lang('appointment_added_to_your_plan')); - $provider_message = new Text(lang('appointment_link_description')); - } - else - { - $customer_title = new Text(lang('appointment_details_changed')); - $customer_message = new Text(''); - $provider_title = new Text(lang('appointment_changes_saved')); - $provider_message = new Text(''); - } - - $customer_link = new Url(site_url('appointments/index/' . $appointment['hash'])); - $provider_link = new Url(site_url('backend/index/' . $appointment['hash'])); - - $send_customer = $this->settings_model->get_setting('customer_notifications'); - - $this->load->library('ics_file'); - $ics_stream = $this->ics_file->get_stream($appointment, $service, $provider, $customer); - - if ((bool)$send_customer === TRUE) - { - $email->sendAppointmentDetails($appointment, $provider, - $service, $customer, $settings, $customer_title, - $customer_message, $customer_link, new Email($customer['email']), new Text($ics_stream)); - } - - if ($send_provider == TRUE) - { - $email->sendAppointmentDetails($appointment, $provider, - $service, $customer, $settings, $provider_title, - $provider_message, $provider_link, new Email($provider['email']), new Text($ics_stream)); - } - - // Notify admins - $admins = $this->admins_model->get_batch(); - - foreach($admins as $admin) - { - if (!$admin['settings']['notifications'] === '0') - { - continue; - } - - $email->sendAppointmentDetails($appointment, $provider, - $service, $customer, $settings, $provider_title, - $provider_message, $provider_link, new Email($admin['email']), new Text($ics_stream)); - } - - // Notify secretaries - $secretaries = $this->secretaries_model->get_batch(); - - foreach($secretaries as $secretary) - { - if (!$secretary['settings']['notifications'] === '0') - { - continue; - } - - if (in_array($provider['id'], $secretary['providers'])) - { - continue; - } - - $email->sendAppointmentDetails($appointment, $provider, - $service, $customer, $settings, $provider_title, - $provider_message, $provider_link, new Email($secretary['email']), new Text($ics_stream)); - } - } - catch (Exception $exception) - { - $warnings[] = [ - 'message' => $exception->getMessage(), - 'trace' => config('debug') ? $exception->getTrace() : [] - ]; - } - - if (empty($warnings)) - { - $response = AJAX_SUCCESS; - } - else - { - $response = ['warnings' => $warnings]; - } + $response = AJAX_SUCCESS; } catch (Exception $exception) { @@ -1032,10 +907,10 @@ class Backend_api extends CI_Controller { $this->load->model('customers_model'); $customer = json_decode($this->input->post('customer'), TRUE); - $required_privilegesileges = ( ! isset($customer['id'])) + $required_privileges = ( ! isset($customer['id'])) ? $this->privileges[PRIV_CUSTOMERS]['add'] : $this->privileges[PRIV_CUSTOMERS]['edit']; - if ($required_privilegesileges == FALSE) + if ($required_privileges == FALSE) { throw new Exception('You do not have the required privileges for this task.'); } @@ -1105,10 +980,10 @@ class Backend_api extends CI_Controller { $this->load->model('services_model'); $service = json_decode($this->input->post('service'), TRUE); - $required_privilegesileges = ( ! isset($service['id'])) + $required_privileges = ( ! isset($service['id'])) ? $this->privileges[PRIV_SERVICES]['add'] : $this->privileges[PRIV_SERVICES]['edit']; - if ($required_privilegesileges == FALSE) + if ($required_privileges == FALSE) { throw new Exception('You do not have the required privileges for this task.'); } @@ -1215,10 +1090,10 @@ class Backend_api extends CI_Controller { $this->load->model('services_model'); $category = json_decode($this->input->post('category'), TRUE); - $required_privilegesileges = ( ! isset($category['id'])) + $required_privileges = ( ! isset($category['id'])) ? $this->privileges[PRIV_SERVICES]['add'] : $this->privileges[PRIV_SERVICES]['edit']; - if ($required_privilegesileges == FALSE) + if ($required_privileges == FALSE) { throw new Exception('You do not have the required privileges for this task.'); } diff --git a/application/controllers/api/v1/API_V1_Controller.php b/application/controllers/api/v1/API_V1_Controller.php index b27e529b..4107d45f 100644 --- a/application/controllers/api/v1/API_V1_Controller.php +++ b/application/controllers/api/v1/API_V1_Controller.php @@ -62,7 +62,7 @@ class API_V1_Controller extends CI_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -142,7 +142,7 @@ class API_V1_Controller extends CI_Controller { * * @param Exception $exception Thrown exception to be outputted. */ - protected function _handleException(Exception $exception) + protected function handle_exception(Exception $exception) { $error = [ 'code' => $exception->getCode() ?: 500, @@ -166,7 +166,7 @@ class API_V1_Controller extends CI_Controller { * * @throws \EA\Engine\Api\V1\Exception */ - protected function _throwRecordNotFound() + protected function throw_record_not_found() { throw new \EA\Engine\Api\V1\Exception('The requested record was not found!', 404, 'Not Found'); } diff --git a/application/controllers/api/v1/Admins.php b/application/controllers/api/v1/Admins.php index 3c4cea7c..d2dcaf51 100644 --- a/application/controllers/api/v1/Admins.php +++ b/application/controllers/api/v1/Admins.php @@ -76,7 +76,7 @@ class Admins extends API_V1_Controller { if ($id !== NULL && count($admins) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $response = new Response($admins); @@ -92,7 +92,7 @@ class Admins extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -123,7 +123,7 @@ class Admins extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -141,7 +141,7 @@ class Admins extends API_V1_Controller { if ($id !== NULL && count($batch) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $request = new Request(); @@ -158,7 +158,7 @@ class Admins extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -182,7 +182,7 @@ class Admins extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } } diff --git a/application/controllers/api/v1/Appointments.php b/application/controllers/api/v1/Appointments.php index d479dab8..4a7ae2b8 100644 --- a/application/controllers/api/v1/Appointments.php +++ b/application/controllers/api/v1/Appointments.php @@ -37,6 +37,8 @@ use EA\Engine\Types\NonEmptyText; * @property Customers_Model $customers_model * @property Settings_Model $settings_model * @property Timezones $timezones + * @property Notifications $notifications + * @property Synchronization $synchronization * @property Roles_Model $roles_model * @property Secretaries_Model $secretaries_model * @property Admins_Model $admins_model @@ -59,6 +61,12 @@ class Appointments extends API_V1_Controller { { parent::__construct(); $this->load->model('appointments_model'); + $this->load->model('services_model'); + $this->load->model('providers_model'); + $this->load->model('customers_model'); + $this->load->model('settings_model'); + $this->load->library('synchronization'); + $this->load->library('notifications'); $this->parser = new \EA\Engine\Api\V1\Parsers\Appointments; } @@ -84,7 +92,7 @@ class Appointments extends API_V1_Controller { if ($id !== NULL && count($appointments) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $response = new Response($appointments); @@ -100,7 +108,7 @@ class Appointments extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -109,8 +117,6 @@ class Appointments extends API_V1_Controller { */ public function post() { - $this->load->model('services_model'); - try { // Insert the appointment to the database. @@ -138,6 +144,20 @@ class Appointments extends API_V1_Controller { $id = $this->appointments_model->add($appointment); + $service = $this->services_model->get_row($appointment['id_services']); + $provider = $this->providers_model->get_row($appointment['id_users_provider']); + $customer = $this->customers_model->get_row($appointment['id_users_customer']); + $settings = [ + 'company_name' => $this->settings_model->get_setting('company_name'), + 'company_email' => $this->settings_model->get_setting('company_email'), + 'company_link' => $this->settings_model->get_setting('company_link'), + 'date_format' => $this->settings_model->get_setting('date_format'), + 'time_format' => $this->settings_model->get_setting('time_format') + ]; + + $this->synchronization->sync_appointment_saved($appointment, $service, $provider, $customer, $settings, FALSE); + $this->notifications->notify_appointment_saved($appointment, $service, $provider, $customer, $settings, FALSE); + // Fetch the new object from the database and return it to the client. $batch = $this->appointments_model->get_batch('id = ' . $id); $response = new Response($batch); @@ -146,7 +166,7 @@ class Appointments extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -164,15 +184,30 @@ class Appointments extends API_V1_Controller { if ($id !== NULL && count($batch) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $request = new Request(); - $updatedAppointment = $request->getBody(); - $baseAppointment = $batch[0]; - $this->parser->decode($updatedAppointment, $baseAppointment); - $updatedAppointment['id'] = $id; - $id = $this->appointments_model->add($updatedAppointment); + $updated_appointment = $request->getBody(); + $base_appointment = $batch[0]; + $this->parser->decode($updated_appointment, $base_appointment); + $updated_appointment['id'] = $id; + $id = $this->appointments_model->add($updated_appointment); + + $service = $this->services_model->get_row($updated_appointment['id_services']); + $provider = $this->providers_model->get_row($updated_appointment['id_users_provider']); + $customer = $this->customers_model->get_row($updated_appointment['id_users_customer']); + $settings = [ + 'company_name' => $this->settings_model->get_setting('company_name'), + 'company_email' => $this->settings_model->get_setting('company_email'), + 'company_link' => $this->settings_model->get_setting('company_link'), + 'date_format' => $this->settings_model->get_setting('date_format'), + 'time_format' => $this->settings_model->get_setting('time_format') + ]; + + $this->synchronization->sync_appointment_saved($updated_appointment, $service, $provider, $customer, $settings, TRUE); + $this->notifications->notify_appointment_saved($updated_appointment, $service, $provider, $customer, $settings, TRUE); + // Fetch the updated object from the database and return it to the client. $batch = $this->appointments_model->get_batch('id = ' . $id); @@ -181,7 +216,7 @@ class Appointments extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -194,8 +229,23 @@ class Appointments extends API_V1_Controller { { try { + $appointment = $this->appointments_model->get_row($id); + $service = $this->services_model->get_row($appointment['id_services']); + $provider = $this->providers_model->get_row($appointment['id_users_provider']); + $customer = $this->customers_model->get_row($appointment['id_users_customer']); + $settings = [ + 'company_name' => $this->settings_model->get_setting('company_name'), + 'company_email' => $this->settings_model->get_setting('company_email'), + 'company_link' => $this->settings_model->get_setting('company_link'), + 'date_format' => $this->settings_model->get_setting('date_format'), + 'time_format' => $this->settings_model->get_setting('time_format') + ]; + $this->appointments_model->delete($id); + $this->synchronization->sync_appointment_deleted($appointment, $provider); + $this->notifications->notify_appointment_deleted($appointment, $service, $provider, $customer, $settings); + $response = new Response([ 'code' => 200, 'message' => 'Record was deleted successfully!' @@ -205,7 +255,7 @@ class Appointments extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } } diff --git a/application/controllers/api/v1/Availabilities.php b/application/controllers/api/v1/Availabilities.php index a3501637..a7840228 100644 --- a/application/controllers/api/v1/Availabilities.php +++ b/application/controllers/api/v1/Availabilities.php @@ -66,8 +66,8 @@ class Availabilities extends API_V1_Controller { { try { - $providerId = new UnsignedInteger($this->input->get('providerId')); - $serviceId = new UnsignedInteger($this->input->get('serviceId')); + $provider_id = new UnsignedInteger($this->input->get('providerId')); + $service_id = new UnsignedInteger($this->input->get('serviceId')); if ($this->input->get('date')) { @@ -78,49 +78,49 @@ class Availabilities extends API_V1_Controller { $date = new DateTime(); } - $provider = $this->providers_model->get_row($providerId->get()); - $service = $this->services_model->get_row($serviceId->get()); + $provider = $this->providers_model->get_row($provider_id->get()); + $service = $this->services_model->get_row($service_id->get()); - $emptyPeriods = $this->_getProviderAvailableTimePeriods($providerId->get(), + $empty_periods = $this->get_provider_available_time_periods($provider_id->get(), $date->format('Y-m-d'), []); - $availableHours = $this->_calculateAvailableHours($emptyPeriods, + $available_hours = $this->calculate_available_hours($empty_periods, $date->format('Y-m-d'), $service['duration'], FALSE, $service['availabilities_type']); if ($service['attendants_number'] > 1) { - $availableHours = $this->_getMultipleAttendantsHours($date->format('Y-m-d'), $service, $provider); + $available_hours = $this->get_multiple_attendants_hours($date->format('Y-m-d'), $service, $provider); } - // If the selected date is today, remove past hours. It is important include the timeout before - // booking that is set in the back-office the system. Normally we might want the customer to book - // an appointment that is at least half or one hour from now. The setting is stored in minutes. + // If the selected date is today, remove past hours. It is important include the timeout before booking + // that is set in the back-office the system. Normally we might want the customer to book an appointment + // that is at least half or one hour from now. The setting is stored in minutes. if ($date->format('Y-m-d') === date('Y-m-d')) { - $bookAdvanceTimeout = $this->settings_model->get_setting('book_advance_timeout'); + $book_advance_timeout = $this->settings_model->get_setting('book_advance_timeout'); - foreach ($availableHours as $index => $value) + foreach ($available_hours as $index => $value) { - $availableHour = strtotime($value); - $currentHour = strtotime('+' . $bookAdvanceTimeout . ' minutes', strtotime('now')); - if ($availableHour <= $currentHour) + $available_hour = strtotime($value); + $currentHour = strtotime('+' . $book_advance_timeout . ' minutes', strtotime('now')); + if ($available_hour <= $currentHour) { - unset($availableHours[$index]); + unset($available_hours[$index]); } } } - $availableHours = array_values($availableHours); - sort($availableHours, SORT_STRING); - $availableHours = array_values($availableHours); + $available_hours = array_values($available_hours); + sort($available_hours, SORT_STRING); + $available_hours = array_values($available_hours); $this->output ->set_content_type('application/json') - ->set_output(json_encode($availableHours)); + ->set_output(json_encode($available_hours)); } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -139,7 +139,7 @@ class Availabilities extends API_V1_Controller { * * @return array Returns an array with the available time periods of the provider. */ - protected function _getProviderAvailableTimePeriods( + protected function get_provider_available_time_periods( $provider_id, $selected_date, $exclude_appointments = [] @@ -342,7 +342,7 @@ class Availabilities extends API_V1_Controller { * * @return array Returns an array with the available hours for the appointment. */ - protected function _calculateAvailableHours( + protected function calculate_available_hours( array $empty_periods, $selected_date, $service_duration, @@ -385,7 +385,7 @@ class Availabilities extends API_V1_Controller { * * @return array Returns the available hours array. */ - protected function _getMultipleAttendantsHours( + protected function get_multiple_attendants_hours( $selected_date, $service, $provider @@ -412,8 +412,8 @@ class Availabilities extends API_V1_Controller { ] ]; - $periods = $this->_removeBreaks($selected_date, $periods, $working_hours['breaks']); - $periods = $this->_removeUnavailabilities($periods, $unavailabilities); + $periods = $this->remove_breaks($selected_date, $periods, $working_hours['breaks']); + $periods = $this->remove_unavailabilities($periods, $unavailabilities); $hours = []; @@ -455,7 +455,7 @@ class Availabilities extends API_V1_Controller { * * @return array Returns the available time periods without the breaks. */ - public function _removeBreaks($selected_date, $periods, $breaks) + public function remove_breaks($selected_date, $periods, $breaks) { if ( ! $breaks) { @@ -517,7 +517,7 @@ class Availabilities extends API_V1_Controller { * * @return array Returns the available time periods without the unavailabilities. */ - public function _removeUnavailabilities($periods, $unavailabilities) + public function remove_unavailabilities($periods, $unavailabilities) { foreach ($unavailabilities as $unavailability) { diff --git a/application/controllers/api/v1/Categories.php b/application/controllers/api/v1/Categories.php index eb5a6649..3e1c8b86 100644 --- a/application/controllers/api/v1/Categories.php +++ b/application/controllers/api/v1/Categories.php @@ -76,7 +76,7 @@ class Categories extends API_V1_Controller { if ($id !== NULL && count($categories) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $response = new Response($categories); @@ -92,7 +92,7 @@ class Categories extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -123,7 +123,7 @@ class Categories extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -141,15 +141,15 @@ class Categories extends API_V1_Controller { if ($id !== NULL && count($batch) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $request = new Request(); - $updatedCategory = $request->getBody(); - $baseCategory = $batch[0]; - $this->parser->decode($updatedCategory, $baseCategory); - $updatedCategory['id'] = $id; - $id = $this->services_model->add_category($updatedCategory); + $updated_category = $request->getBody(); + $base_category = $batch[0]; + $this->parser->decode($updated_category, $base_category); + $updated_category['id'] = $id; + $id = $this->services_model->add_category($updated_category); // Fetch the updated object from the database and return it to the client. $batch = $this->services_model->get_all_categories('id = ' . $id); @@ -158,7 +158,7 @@ class Categories extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -182,7 +182,7 @@ class Categories extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } } diff --git a/application/controllers/api/v1/Customers.php b/application/controllers/api/v1/Customers.php index e2d9a332..ce95ea8e 100644 --- a/application/controllers/api/v1/Customers.php +++ b/application/controllers/api/v1/Customers.php @@ -76,7 +76,7 @@ class Customers extends API_V1_Controller { if ($id !== NULL && count($customers) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $response = new Response($customers); @@ -92,7 +92,7 @@ class Customers extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -123,7 +123,7 @@ class Customers extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -141,15 +141,15 @@ class Customers extends API_V1_Controller { if ($id !== NULL && count($batch) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $request = new Request(); - $updatedCustomer = $request->getBody(); - $baseCustomer = $batch[0]; - $this->parser->decode($updatedCustomer, $baseCustomer); - $updatedCustomer['id'] = $id; - $id = $this->customers_model->add($updatedCustomer); + $updated_customer = $request->getBody(); + $base_customer = $batch[0]; + $this->parser->decode($updated_customer, $base_customer); + $updated_customer['id'] = $id; + $id = $this->customers_model->add($updated_customer); // Fetch the updated object from the database and return it to the client. $batch = $this->customers_model->get_batch('id = ' . $id); @@ -158,7 +158,7 @@ class Customers extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -182,7 +182,7 @@ class Customers extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } } diff --git a/application/controllers/api/v1/Providers.php b/application/controllers/api/v1/Providers.php index 2918e053..fe342cbb 100644 --- a/application/controllers/api/v1/Providers.php +++ b/application/controllers/api/v1/Providers.php @@ -76,7 +76,7 @@ class Providers extends API_V1_Controller { if ($id !== NULL && count($providers) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $response = new Response($providers); @@ -92,7 +92,7 @@ class Providers extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -123,7 +123,7 @@ class Providers extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -141,15 +141,15 @@ class Providers extends API_V1_Controller { if ($id !== NULL && count($batch) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $request = new Request(); - $updatedProvider = $request->getBody(); - $baseProvider = $batch[0]; - $this->parser->decode($updatedProvider, $baseProvider); - $updatedProvider['id'] = $id; - $id = $this->providers_model->add($updatedProvider); + $updated_provider = $request->getBody(); + $base_provider = $batch[0]; + $this->parser->decode($updated_provider, $base_provider); + $updated_provider['id'] = $id; + $id = $this->providers_model->add($updated_provider); // Fetch the updated object from the database and return it to the client. $batch = $this->providers_model->get_batch('id = ' . $id); @@ -158,7 +158,7 @@ class Providers extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -182,7 +182,7 @@ class Providers extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } } diff --git a/application/controllers/api/v1/Secretaries.php b/application/controllers/api/v1/Secretaries.php index 605fd711..fcff6060 100644 --- a/application/controllers/api/v1/Secretaries.php +++ b/application/controllers/api/v1/Secretaries.php @@ -76,7 +76,7 @@ class Secretaries extends API_V1_Controller { if ($id !== NULL && count($secretaries) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $response = new Response($secretaries); @@ -92,7 +92,7 @@ class Secretaries extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -123,7 +123,7 @@ class Secretaries extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -141,15 +141,15 @@ class Secretaries extends API_V1_Controller { if ($id !== NULL && count($batch) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $request = new Request(); - $updatedSecretary = $request->getBody(); - $baseSecretary = $batch[0]; - $this->parser->decode($updatedSecretary, $baseSecretary); - $updatedSecretary['id'] = $id; - $id = $this->secretaries_model->add($updatedSecretary); + $updated_secretary = $request->getBody(); + $base_secretary = $batch[0]; + $this->parser->decode($updated_secretary, $base_secretary); + $updated_secretary['id'] = $id; + $id = $this->secretaries_model->add($updated_secretary); // Fetch the updated object from the database and return it to the client. $batch = $this->secretaries_model->get_batch('id = ' . $id); @@ -158,7 +158,7 @@ class Secretaries extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -182,7 +182,7 @@ class Secretaries extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } } diff --git a/application/controllers/api/v1/Services.php b/application/controllers/api/v1/Services.php index 6aa9ccf1..0aace4a0 100644 --- a/application/controllers/api/v1/Services.php +++ b/application/controllers/api/v1/Services.php @@ -76,7 +76,7 @@ class Services extends API_V1_Controller { if ($id !== NULL && count($services) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $response = new Response($services); @@ -92,7 +92,7 @@ class Services extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -123,7 +123,7 @@ class Services extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -141,15 +141,15 @@ class Services extends API_V1_Controller { if ($id !== NULL && count($batch) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $request = new Request(); - $updatedService = $request->getBody(); - $baseService = $batch[0]; - $this->parser->decode($updatedService, $baseService); - $updatedService['id'] = $id; - $id = $this->services_model->add($updatedService); + $updated_service = $request->getBody(); + $base_service = $batch[0]; + $this->parser->decode($updated_service, $base_service); + $updated_service['id'] = $id; + $id = $this->services_model->add($updated_service); // Fetch the updated object from the database and return it to the client. $batch = $this->services_model->get_batch('id = ' . $id); @@ -158,7 +158,7 @@ class Services extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } @@ -182,7 +182,7 @@ class Services extends API_V1_Controller { } catch (Exception $exception) { - $this->_handleException($exception); + $this->handle_exception($exception); } } } diff --git a/application/controllers/api/v1/Settings.php b/application/controllers/api/v1/Settings.php index 19a82f28..2936d4f4 100644 --- a/application/controllers/api/v1/Settings.php +++ b/application/controllers/api/v1/Settings.php @@ -87,7 +87,7 @@ class Settings extends API_V1_Controller { if (empty($setting)) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } unset($setting['id']); @@ -110,7 +110,7 @@ class Settings extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -138,7 +138,7 @@ class Settings extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -162,7 +162,7 @@ class Settings extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } } diff --git a/application/controllers/api/v1/Unavailabilities.php b/application/controllers/api/v1/Unavailabilities.php index bc2cc2c9..c1d81b9c 100644 --- a/application/controllers/api/v1/Unavailabilities.php +++ b/application/controllers/api/v1/Unavailabilities.php @@ -76,7 +76,7 @@ class Unavailabilities extends API_V1_Controller { if ($id !== NULL && count($unavailabilities) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $response = new Response($unavailabilities); @@ -92,7 +92,7 @@ class Unavailabilities extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -123,7 +123,7 @@ class Unavailabilities extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -141,7 +141,7 @@ class Unavailabilities extends API_V1_Controller { if ($id !== NULL && count($batch) === 0) { - $this->_throwRecordNotFound(); + $this->throw_record_not_found(); } $request = new Request(); @@ -158,7 +158,7 @@ class Unavailabilities extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } @@ -182,7 +182,7 @@ class Unavailabilities extends API_V1_Controller { } catch (Exception $exception) { - exit($this->_handleException($exception)); + exit($this->handle_exception($exception)); } } } diff --git a/application/libraries/Availability.php b/application/libraries/Availability.php new file mode 100644 index 00000000..08daad31 --- /dev/null +++ b/application/libraries/Availability.php @@ -0,0 +1,552 @@ + + * @copyright Copyright (c) 2013 - 2020, Alex Tselegidis + * @license http://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link http://easyappointments.org + * @since v1.4.0 + * ---------------------------------------------------------------------------- */ + + +/** + * Class Availability + * + * Handles the availability generation of providers, based on their working plan and their schedule. + */ +class Availability { + /** + * @var CI_Controller + */ + protected $CI; + + /** + * Availability constructor. + */ + public function __construct() + { + $this->CI =& get_instance(); + $this->CI->load->model('providers_model'); + $this->CI->load->model('secretaries_model'); + $this->CI->load->model('secretaries_model'); + $this->CI->load->model('admins_model'); + $this->CI->load->model('appointments_model'); + $this->CI->load->model('settings_model'); + $this->CI->load->library('ics_file'); + } + + /** + * Get the available hours of a provider. + * + * @param string $date Selected date (Y-m-d). + * @param array $service Service record. + * @param array $provider Provider record. + * @param int|null $exclude_appointment_id Exclude an appointment from the availability generation. + * + * @return array + * + * @throws Exception + */ + public function get_available_hours($date, $service, $provider, $exclude_appointment_id = NULL) + { + $available_periods = $this->get_available_periods($date, $provider, $exclude_appointment_id); + + $available_hours = $this->generate_available_hours($date, $service, $available_periods); + + if ($service['attendants_number'] > 1) + { + $available_hours = $this->consider_multiple_attendants($date, $service, $provider); + } + + return $this->consider_book_advance_timeout($date, $available_hours, $provider); + } + + /** + * 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 + * available 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 string $date Select date string. + * @param array $provider Provider record. + * @param int|null $exclude_appointment_id Exclude an appointment from the availability generation. + * + * @return array Returns an array with the available time periods of the provider. + * + * @throws Exception + */ + public function get_available_periods( + $date, + $provider, + $exclude_appointment_id = NULL + ) + { + // Get the service, provider's working plan and provider appointments. + $working_plan = json_decode($provider['settings']['working_plan'], TRUE); + + // Get the provider's working plan exceptions. + $working_plan_exceptions = json_decode($provider['settings']['working_plan_exceptions'], TRUE); + + $conditions = [ + 'id_users_provider' => $provider['id'], + ]; + + // Sometimes it might be necessary to exclude an appointment from the calculation (e.g. when editing an + // existing appointment). + if ($exclude_appointment_id) + { + $conditions['id !='] = $exclude_appointment_id; + } + + $appointments = $this->CI->appointments_model->get_batch($conditions); + + // Find the empty spaces on the plan. The first split between the plan is due to a break (if any). After that + // every reserved appointment is considered to be a taken space in the plan. + $date_working_plan = $working_plan[strtolower(date('l', strtotime($date)))]; + + // Search if the $date is an custom availability period added outside the normal working plan. + if (isset($working_plan_exceptions[$date])) + { + $date_working_plan = $working_plan_exceptions[$date]; + } + + $periods = []; + + if (isset($date_working_plan['breaks'])) + { + $periods[] = [ + 'start' => $date_working_plan['start'], + 'end' => $date_working_plan['end'] + ]; + + $day_start = new DateTime($date_working_plan['start']); + $day_end = new DateTime($date_working_plan['end']); + + // Split the working plan to available time periods that do not contain the breaks in them. + foreach ($date_working_plan['breaks'] as $index => $break) + { + $break_start = new DateTime($break['start']); + $break_end = new DateTime($break['end']); + + if ($break_start < $day_start) + { + $break_start = $day_start; + } + + if ($break_end > $day_end) + { + $break_end = $day_end; + } + + if ($break_start >= $break_end) + { + continue; + } + + foreach ($periods as $key => $period) + { + $period_start = new DateTime($period['start']); + $period_end = new DateTime($period['end']); + + $remove_current_period = FALSE; + + if ($break_start > $period_start && $break_start < $period_end && $break_end > $period_start) + { + $periods[] = [ + 'start' => $period_start->format('H:i'), + 'end' => $break_start->format('H:i') + ]; + + $remove_current_period = TRUE; + } + + if ($break_start < $period_end && $break_end > $period_start && $break_end < $period_end) + { + $periods[] = [ + 'start' => $break_end->format('H:i'), + 'end' => $period_end->format('H:i') + ]; + + $remove_current_period = TRUE; + } + + if ($break_start == $period_start && $break_end == $period_end) + { + $remove_current_period = TRUE; + } + + if ($remove_current_period) + { + unset($periods[$key]); + } + } + } + } + + // Break the empty periods with the reserved appointments. + foreach ($appointments as $appointment) + { + foreach ($periods as $index => &$period) + { + $appointment_start = new DateTime($appointment['start_datetime']); + $appointment_end = new DateTime($appointment['end_datetime']); + + if ($appointment_start >= $appointment_end) + { + continue; + } + + $period_start = new DateTime($date . ' ' . $period['start']); + $period_end = new DateTime($date . ' ' . $period['end']); + + if ($appointment_start <= $period_start && $appointment_end <= $period_end && $appointment_end <= $period_start) + { + // The appointment does not belong in this time period, so we will not change anything. + continue; + } + else + { + if ($appointment_start <= $period_start && $appointment_end <= $period_end && $appointment_end >= $period_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'] = $appointment_end->format('H:i'); + } + else + { + if ($appointment_start >= $period_start && $appointment_end < $period_end) + { + // The appointment is inside the time period, so we will split the period into two new + // others. + unset($periods[$index]); + + $periods[] = [ + 'start' => $period_start->format('H:i'), + 'end' => $appointment_start->format('H:i') + ]; + + $periods[] = [ + 'start' => $appointment_end->format('H:i'), + 'end' => $period_end->format('H:i') + ]; + } + else if ($appointment_start == $period_start && $appointment_end == $period_end) + { + unset($periods[$index]); // The whole period is blocked so remove it from the available periods array. + } + else + { + if ($appointment_start >= $period_start && $appointment_end >= $period_start && $appointment_start <= $period_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'] = $appointment_start->format('H:i'); + } + else + { + if ($appointment_start >= $period_start && $appointment_end >= $period_end && $appointment_start >= $period_end) + { + // The appointment does not belong in the period so do not change anything. + continue; + } + else + { + if ($appointment_start <= $period_start && $appointment_end >= $period_end && $appointment_start <= $period_end) + { + // The appointment is bigger than the period, so this period needs to be removed. + unset($periods[$index]); + } + } + } + } + } + } + } + } + + return array_values($periods); + } + + + /** + * Calculate the available appointment hours. + * + * Calculate the available appointment hours for the given date. The empty spaces + * are broken down to 15 min and if the service fit in each quarter then a new + * available hour is added to the "$available_hours" array. + * + * @param string $date Selected date (Y-m-d). + * @param array $service Service record. + * @param array $empty_periods Empty periods as generated by the "get_provider_available_time_periods" + * method. + * + * @return array Returns an array with the available hours for the appointment. + * + * @throws Exception + */ + protected function generate_available_hours( + $date, + $service, + $empty_periods + ) + { + $available_hours = []; + + foreach ($empty_periods as $period) + { + $start_hour = new DateTime($date . ' ' . $period['start']); + $end_hour = new DateTime($date . ' ' . $period['end']); + $interval = $service['availabilities_type'] === AVAILABILITIES_TYPE_FIXED ? (int)$service['duration'] : 15; + + $current_hour = $start_hour; + $diff = $current_hour->diff($end_hour); + + while (($diff->h * 60 + $diff->i) >= (int)$service['duration']) + { + $available_hours[] = $current_hour->format('H:i'); + $current_hour->add(new DateInterval('PT' . $interval . 'M')); + $diff = $current_hour->diff($end_hour); + } + } + + return $available_hours; + } + + /** + * Get multiple attendants hours. + * + * This method will add the additional appointment hours whenever a service accepts multiple attendants. + * + * @param string $date Selected date (Y-m-d). + * @param array $service Service record. + * @param array $provider Provider record. + * + * @return array Returns the available hours array. + * + * @throws Exception + */ + protected function consider_multiple_attendants( + $date, + $service, + $provider + ) + { + $unavailability_events = $this->CI->appointments_model->get_batch([ + 'is_unavailable' => TRUE, + 'DATE(start_datetime)' => $date, + 'id_users_provider' => $provider['id'] + ]); + + $working_plan = json_decode($provider['settings']['working_plan'], TRUE); + $working_day = strtolower(date('l', strtotime($date))); + $working_hours = $working_plan[$working_day]; + + $periods = [ + [ + 'start' => new DateTime($date . ' ' . $working_hours['start']), + 'end' => new DateTime($date . ' ' . $working_hours['end']) + ] + ]; + + $periods = $this->remove_breaks($date, $periods, $working_hours['breaks']); + $periods = $this->remove_unavailability_events($periods, $unavailability_events); + + $hours = []; + + $interval_value = $service['availabilities_type'] == AVAILABILITIES_TYPE_FIXED ? $service['duration'] : '15'; + $interval = new DateInterval('PT' . (int)$interval_value . 'M'); + $duration = new DateInterval('PT' . (int)$service['duration'] . 'M'); + + foreach ($periods as $period) + { + $slot_start = clone $period['start']; + $slot_end = clone $slot_start; + $slot_end->add($duration); + + while ($slot_end <= $period['end']) + { + // Check reserved attendants for this time slot and see if current attendants fit. + $appointment_attendants_number = $this->CI->appointments_model->get_attendants_number_for_period( + $slot_start, + $slot_end, + $service['id'] + ); + + if ($appointment_attendants_number < $service['attendants_number']) + { + $hours[] = $slot_start->format('H:i'); + } + + $slot_start->add($interval); + $slot_end->add($interval); + } + } + + return $hours; + } + + /** + * Remove breaks from available time periods. + * + * @param string $selected_date Selected data (Y-m-d format). + * @param array $periods Time periods of the current date. + * @param array $breaks Breaks array for the current date. + * + * @return array Returns the available time periods without the breaks. + * @throws Exception + */ + public function remove_breaks($selected_date, $periods, $breaks) + { + if ( ! $breaks) + { + return $periods; + } + + foreach ($breaks as $break) + { + $break_start = new DateTime($selected_date . ' ' . $break['start']); + $break_end = new DateTime($selected_date . ' ' . $break['end']); + + foreach ($periods as &$period) + { + $period_start = $period['start']; + $period_end = $period['end']; + + if ($break_start <= $period_start && $break_end >= $period_start && $break_end <= $period_end) + { + // left + $period['start'] = $break_end; + continue; + } + + if ($break_start >= $period_start && $break_start <= $period_end && $break_end >= $period_start && $break_end <= $period_end) + { + // middle + $period['end'] = $break_start; + $periods[] = [ + 'start' => $break_end, + 'end' => $period_end + ]; + continue; + } + + if ($break_start >= $period_start && $break_start <= $period_end && $break_end >= $period_end) + { + // right + $period['end'] = $break_start; + continue; + } + + if ($break_start <= $period_start && $break_end >= $period_end) + { + // break contains period + $period['start'] = $break_end; + continue; + } + } + } + + return $periods; + } + + /** + * Remove the unavailability entries from the available time periods of the selected date. + * + * @param array $periods Available time periods. + * @param array $unavailability_events Unavailability events of the current date. + * + * @return array Returns the available time periods without the unavailability events. + * + * @throws Exception + */ + public function remove_unavailability_events($periods, $unavailability_events) + { + foreach ($unavailability_events as $unavailability_event) + { + $unavailability_start = new DateTime($unavailability_event['start_datetime']); + $unavailability_end = new DateTime($unavailability_event['end_datetime']); + + foreach ($periods as &$period) + { + $period_start = $period['start']; + $period_end = $period['end']; + + if ($unavailability_start <= $period_start && $unavailability_end >= $period_start && $unavailability_end <= $period_end) + { + // left + $period['start'] = $unavailability_end; + continue; + } + + if ($unavailability_start >= $period_start && $unavailability_start <= $period_end && $unavailability_end >= $period_start && $unavailability_end <= $period_end) + { + // middle + $period['end'] = $unavailability_start; + $periods[] = [ + 'start' => $unavailability_end, + 'end' => $period_end + ]; + continue; + } + + if ($unavailability_start >= $period_start && $unavailability_start <= $period_end && $unavailability_end >= $period_end) + { + // right + $period['end'] = $unavailability_start; + continue; + } + + if ($unavailability_start <= $period_start && $unavailability_end >= $period_end) + { + // Unavailability contains period + $period['start'] = $unavailability_end; + continue; + } + } + } + + return $periods; + } + + /** + * Consider the book advance timeout and remove available hours that have passed the threshold. + * + * If the selected date is today, remove past hours. It is important include the timeout before booking + * that is set in the back-office the system. Normally we might want the customer to book an appointment + * that is at least half or one hour from now. The setting is stored in minutes. + * + * @param string $selected_date The selected date. + * @param array $available_hours Already generated available hours. + * @param array $provider Provider information. + * + * @return array Returns the updated available hours. + * + * @throws Exception + */ + protected function consider_book_advance_timeout($selected_date, $available_hours, $provider) + { + $provider_timezone = new DateTimeZone($provider['timezone']); + + $book_advance_timeout = $this->CI->settings_model->get_setting('book_advance_timeout'); + + $threshold = new DateTime('+' . $book_advance_timeout . ' minutes', $provider_timezone); + + foreach ($available_hours as $index => $value) + { + $available_hour = new DateTime($selected_date . ' ' . $value, $provider_timezone); + + if ($available_hour->getTimestamp() <= $threshold->getTimestamp()) + { + unset($available_hours[$index]); + } + } + + $available_hours = array_values($available_hours); + sort($available_hours, SORT_STRING); + return array_values($available_hours); + } +} diff --git a/application/libraries/Notifications.php b/application/libraries/Notifications.php new file mode 100644 index 00000000..1b675669 --- /dev/null +++ b/application/libraries/Notifications.php @@ -0,0 +1,227 @@ + + * @copyright Copyright (c) 2013 - 2020, Alex Tselegidis + * @license http://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link http://easyappointments.org + * @since v1.4.0 + * ---------------------------------------------------------------------------- */ + +use EA\Engine\Notifications\Email as EmailClient; +use EA\Engine\Types\Email; +use EA\Engine\Types\Text; +use EA\Engine\Types\Url; + +/** + * Class Notifications + * + * Handles the system notifications (mostly related to scheduling changes). + */ +class Notifications { + /** + * @var CI_Controller + */ + protected $CI; + + /** + * Notifications constructor. + */ + public function __construct() + { + $this->CI =& get_instance(); + $this->CI->load->model('providers_model'); + $this->CI->load->model('secretaries_model'); + $this->CI->load->model('secretaries_model'); + $this->CI->load->model('admins_model'); + $this->CI->load->model('appointments_model'); + $this->CI->load->model('settings_model'); + $this->CI->load->library('ics_file'); + } + + /** + * Send the required notifications, related to an appointment creation/modification. + * + * @param array $appointment Appointment record. + * @param array $service Service record. + * @param array $provider Provider record. + * @param array $customer Customer record. + * @param array $settings Required settings for the notification content. + * @param bool|false $manage_mode + */ + public function notify_appointment_saved($appointment, $service, $provider, $customer, $settings, $manage_mode = FALSE) + { + // Send email notifications to customer and provider. + try + { + $this->CI->config->load('email'); + + $email = new EmailClient($this->CI, $this->CI->config->config); + + if ($manage_mode === FALSE) + { + $customer_title = new Text(lang('appointment_booked')); + $customer_message = new Text(lang('thank_you_for_appointment')); + $provider_title = new Text(lang('appointment_added_to_your_plan')); + $provider_message = new Text(lang('appointment_link_description')); + + } + else + { + $customer_title = new Text(lang('appointment_changes_saved')); + $customer_message = new Text(''); + $provider_title = new Text(lang('appointment_details_changed')); + $provider_message = new Text(''); + } + + $customer_link = new Url(site_url('appointments/index/' . $appointment['hash'])); + $provider_link = new Url(site_url('backend/index/' . $appointment['hash'])); + + $send_customer = filter_var( + $this->CI->settings_model->get_setting('customer_notifications'), + FILTER_VALIDATE_BOOLEAN); + + + $ics_stream = $this->CI->ics_file->get_stream($appointment, $service, $provider, $customer); + + if ($send_customer === TRUE) + { + $email->sendAppointmentDetails($appointment, $provider, + $service, $customer, $settings, $customer_title, + $customer_message, $customer_link, new Email($customer['email']), new Text($ics_stream)); + } + + $send_provider = filter_var( + $this->CI->providers_model->get_setting('notifications', $provider['id']), + FILTER_VALIDATE_BOOLEAN); + + if ($send_provider === TRUE) + { + $email->sendAppointmentDetails($appointment, $provider, + $service, $customer, $settings, $provider_title, + $provider_message, $provider_link, new Email($provider['email']), new Text($ics_stream)); + } + + // Notify admins + $admins = $this->CI->admins_model->get_batch(); + + foreach ($admins as $admin) + { + if ( ! $admin['settings']['notifications'] === '0') + { + continue; + } + + $email->sendAppointmentDetails($appointment, $provider, + $service, $customer, $settings, $provider_title, + $provider_message, $provider_link, new Email($admin['email']), new Text($ics_stream)); + } + + // Notify secretaries + $secretaries = $this->CI->secretaries_model->get_batch(); + + foreach ($secretaries as $secretary) + { + if ( ! $secretary['settings']['notifications'] === '0') + { + continue; + } + + if (in_array($provider['id'], $secretary['providers'])) + { + continue; + } + + $email->sendAppointmentDetails($appointment, $provider, + $service, $customer, $settings, $provider_title, + $provider_message, $provider_link, new Email($secretary['email']), new Text($ics_stream)); + } + } + catch (Exception $exception) + { + log_message('error', $exception->getMessage()); + log_message('error', $exception->getTraceAsString()); + } + } + + /** + * Send the required notifications, related to an appointment removal. + * + * @param array $appointment Appointment record. + * @param array $service Service record. + * @param array $provider Provider record. + * @param array $customer Customer record. + * @param array $settings Required settings for the notification content. + */ + public function notify_appointment_deleted($appointment, $service, $provider, $customer, $settings) + { + // Send email notification to customer and provider. + try + { + $email = new EmailClient($this->CI, $this->CI->config->config); + + $send_provider = filter_var($this->CI->providers_model->get_setting('notifications', $provider['id']), + FILTER_VALIDATE_BOOLEAN); + + if ($send_provider === TRUE) + { + $email->sendDeleteAppointment($appointment, $provider, + $service, $customer, $settings, new Email($provider['email']), + new Text($this->CI->input->post('cancel_reason'))); + } + + $send_customer = filter_var( + $this->CI->settings_model->get_setting('customer_notifications'), + FILTER_VALIDATE_BOOLEAN); + + if ($send_customer === TRUE) + { + $email->sendDeleteAppointment($appointment, $provider, + $service, $customer, $settings, new Email($customer['email']), + new Text($this->CI->input->post('cancel_reason'))); + } + + // Notify admins + $admins = $this->CI->admins_model->get_batch(); + + foreach ($admins as $admin) + { + if ( ! $admin['settings']['notifications'] === '0') + { + continue; + } + + $email->sendDeleteAppointment($appointment, $provider, + $service, $customer, $settings, new Email($admin['email']), + new Text($this->CI->input->post('cancel_reason'))); + } + + // Notify secretaries + $secretaries = $this->CI->secretaries_model->get_batch(); + + foreach ($secretaries as $secretary) + { + if ( ! $secretary['settings']['notifications'] === '0') + { + continue; + } + + if (in_array($provider['id'], $secretary['providers'])) + { + continue; + } + + $email->sendDeleteAppointment($appointment, $provider, + $service, $customer, $settings, new Email($secretary['email']), + new Text($this->CI->input->post('cancel_reason'))); + } + } + catch (Exception $exception) + { + $exceptions[] = $exception; + } + } +} diff --git a/application/libraries/Synchronization.php b/application/libraries/Synchronization.php new file mode 100644 index 00000000..a8a0100d --- /dev/null +++ b/application/libraries/Synchronization.php @@ -0,0 +1,120 @@ + + * @copyright Copyright (c) 2013 - 2020, Alex Tselegidis + * @license http://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link http://easyappointments.org + * @since v1.4.0 + * ---------------------------------------------------------------------------- */ + +/** + * Class Synchronization + * + * Handles the external calendar synchronization. + */ +class Synchronization { + /** + * @var CI_Controller + */ + protected $CI; + + /** + * Synchronization constructor. + */ + public function __construct() + { + $this->CI =& get_instance(); + $this->CI->load->model('providers_model'); + $this->CI->load->model('appointments_model'); + $this->CI->load->library('google_sync'); + } + + /** + * Synchronize changes made to the appointment with external calendars. + * + * @param array $appointment Appointment record. + * @param array $service Service record. + * @param array $provider Provider record. + * @param array $customer Customer record. + * @param array $settings Required settings for the notification content. + * @param bool|false $manage_mode True if the appointment is being edited. + */ + public function sync_appointment_saved($appointment, $service, $provider, $customer, $settings, $manage_mode = FALSE) + { + try + { + $google_sync = filter_var( + $this->CI->providers_model->get_setting('google_sync', $appointment['id_users_provider']), + FILTER_VALIDATE_BOOLEAN + ); + + if ($google_sync === TRUE) + { + $google_token = json_decode( + $this->CI->providers_model->get_setting('google_token', $appointment['id_users_provider'])); + + $this->CI->load->library('google_sync'); + + $this->CI->google_sync->refresh_token($google_token->refresh_token); + + if ($manage_mode === FALSE) + { + // Add appointment to Google Calendar. + $google_event = $this->CI->google_sync->add_appointment($appointment, $provider, + $service, $customer, $settings); + $appointment['id_google_calendar'] = $google_event->id; + $this->CI->appointments_model->add($appointment); + } + else + { + // Update appointment to Google Calendar. + $appointment['id_google_calendar'] = $this->CI->appointments_model + ->get_value('id_google_calendar', $appointment['id']); + + $this->CI->google_sync->update_appointment($appointment, $provider, + $service, $customer, $settings); + } + } + } + catch (Exception $exception) + { + log_message('error', $exception->getMessage()); + log_message('error', $exception->getTraceAsString()); + } + } + + /** + * Synchronize removal of an appointment with external calendars. + * + * @param array $appointment Appointment record. + * @param array $provider Provider record. + */ + public function sync_appointment_deleted($appointment, $provider) + { + if ($appointment['id_google_calendar'] != NULL) + { + try + { + $google_sync = filter_var( + $this->CI->providers_model->get_setting('google_sync', $appointment['id_users_provider']), + FILTER_VALIDATE_BOOLEAN); + + if ($google_sync === TRUE) + { + $google_token = json_decode($this->CI->providers_model->get_setting('google_token', $provider['id'])); + $this->CI->load->library('Google_sync'); + $this->CI->google_sync->refresh_token($google_token->refresh_token); + $this->CI->google_sync->delete_appointment($provider, $appointment['id_google_calendar']); + } + } + catch (Exception $exception) + { + $exceptions[] = $exception; + } + } + } +} diff --git a/application/views/appointments/book.php b/application/views/appointments/book.php index adc8912c..ec8a3e7f 100755 --- a/application/views/appointments/book.php +++ b/application/views/appointments/book.php @@ -10,7 +10,6 @@ - @@ -32,17 +31,17 @@ = $company_name ?>