diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6e019e..7b81fbf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,36 @@ This file contains the code changes that were introduced into each release (starting from v1.1.0) so that is easy for developers to maintain and readjust their custom modifications on the main project codebase. +## [1.4.2] - TBA + +### Added + +- #1004: Add support for line breaks when displaying the service description in the frontend. +- #1040: Support all-day events while syncing with Google Calendar. + +### Fixed + +- #1000: Small fix for the display of the delete button in table view. +- #1011: Working plan exception - details pane shows incorrect details. +- #1023: Backend calendar table events missing or duplicated. +- #1026: The timepicker sliders do not work when using an iOS device. +- #1029: Enhance SMTP functions of PHPMailer. +- #1043: Unavailable events do not block time from services with multiple attendants. +- #1046: Make sure that saving the modifications of a single break does not cancel any pending break edits. +- #1068: Set minimum service duration field value to honor the value of EVENT_MINIMUM_DURATION. +- #1073: Update PHPMailer dependencies. +- #1074: In case of deletion of one appointment, system sends email to admins anyway even if they have email notifications disabled. +- #1092: Javascript RangeError on appointment change causing disabled calendar dates. +- #961: Timezone/UX issue: Wrong day is selected when timezone differs by -1 day. +- #966: Secretaries are getting notification emails for providers that are not assigned to them. +- #980: Missing Pacific (and potentially other) timezones. +- #982: The Any-Provider option might lead to double bookings, if all the providers have the same number of appointments for the selected date. +- #986: Managed to replicate appointment hash collisions. +- #989: Fix Critical mistake resulting in wrong date +- #990: The API availabilities controller throws an error when generating availability for services with multiple attendants. +- #991: Available hours generated with the "Any Provider" option in the booking page, may use the information of a provider that is not assigned to the selected service. +- #993: Add support for PHP8 (vendor packages need to be updated). + ## [1.4.1] - 2020-12-14 ### Added diff --git a/application/config/config.php b/application/config/config.php index ddf01caa..db6d0646 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -8,8 +8,8 @@ | Declare some of the global config values of Easy!Appointments. | */ -$config['version'] = '1.4.1'; // This must be changed manually. -$config['release_label'] = ''; // Leave empty for no title or add Alpha, Beta etc ... +$config['version'] = '1.4.2'; // This must be changed manually. +$config['release_label'] = 'beta.1'; // Leave empty for no title or add Alpha, Beta etc ... $config['debug'] = Config::DEBUG_MODE; /* @@ -82,32 +82,41 @@ $config['url_suffix'] = ''; | */ -$config['language'] = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? - [ - 'ar' => 'arabic', - 'bu' => 'bulgarian', - 'zh' => 'chinese', - 'da' => 'danish', - 'nl' => 'dutch', - 'en' => 'english', - 'fi' => 'finnish', - 'fr' => 'french', - 'de' => 'german', - 'el' => 'greek', - 'he' => 'hebrew', - 'hi' => 'hindi', - 'hu' => 'hungarian', - 'it' => 'italian', - 'ja' => 'japanese', - 'pl' => 'polish', - 'pt' => 'portuguese', - 'ro' => 'romanian', - 'ru' => 'russian', - 'sk' => 'slovak', - 'es' => 'spanish', - 'tr' => 'turkish', - 'sv' => 'swedish' - ][substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2)] +$languages = [ + 'ar' => 'arabic', + 'bu' => 'bulgarian', + 'ca' => 'catalan', + 'zh' => 'chinese', + 'cs' => 'czech', + 'da' => 'danish', + 'nl' => 'dutch', + 'en' => 'english', + 'fi' => 'finnish', + 'fr' => 'french', + 'de' => 'german', + 'el' => 'greek', + 'he' => 'hebrew', + 'hi' => 'hindi', + 'hu' => 'hungarian', + 'it' => 'italian', + 'ja' => 'japanese', + 'fa' => 'persian', + 'lb' => 'luxembourgish', + 'mr' => 'marathi', + 'pl' => 'polish', + 'pt' => 'portuguese', + 'ro' => 'romanian', + 'ru' => 'russian', + 'sk' => 'slovak', + 'es' => 'spanish', + 'sv' => 'swedish', + 'tr' => 'turkish', +]; + +$language_code = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2) : 'en'; + +$config['language'] = isset($_SERVER['HTTP_ACCEPT_LANGUAGE'], $languages[$language_code]) + ? $languages[$language_code] : Config::LANGUAGE; /* @@ -138,8 +147,9 @@ $config['available_languages'] = [ 'hungarian', 'italian', 'japanese', - 'marathi', 'luxembourgish', + 'marathi', + 'persian', 'polish', 'portuguese', 'portuguese-br', @@ -304,7 +314,7 @@ $config['cache_path'] = __DIR__ . '/../../storage/cache/'; | new release. | */ -$config['cache_busting_token'] = '924WX'; +$config['cache_busting_token'] = '624TB'; /* |-------------------------------------------------------------------------- diff --git a/application/config/email.php b/application/config/email.php index 166668d3..0f70a35d 100644 --- a/application/config/email.php +++ b/application/config/email.php @@ -7,6 +7,8 @@ $config['useragent'] = 'Easy!Appointments'; $config['protocol'] = 'mail'; // or 'smtp' $config['mailtype'] = 'html'; // or 'text' +// $config['smtp_debug'] = '0'; // or '1' +// $config['smtp_auth'] = TRUE; //or FALSE for anonymous relay. // $config['smtp_host'] = ''; // $config['smtp_user'] = ''; // $config['smtp_pass'] = ''; diff --git a/application/controllers/Appointments.php b/application/controllers/Appointments.php index ff6c6b4f..8406fea7 100755 --- a/application/controllers/Appointments.php +++ b/application/controllers/Appointments.php @@ -340,7 +340,7 @@ class Appointments extends EA_Controller { // that will provide the requested service. if ($provider_id === ANY_PROVIDER) { - $provider_id = $this->search_any_provider($selected_date, $service_id); + $provider_id = $this->search_any_provider($service_id, $selected_date); if ($provider_id === NULL) { @@ -378,14 +378,15 @@ class Appointments extends EA_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 $date The date to be searched (Y-m-d). + * @param string $hour The hour to be searched (H:i). * * @return int Returns the ID of the provider that can provide the service at the selected date. * * @throws Exception */ - protected function search_any_provider($date, $service_id) + protected function search_any_provider($service_id, $date, $hour = null) { $available_providers = $this->providers_model->get_available_providers(); @@ -404,7 +405,7 @@ class Appointments extends EA_Controller { // Check if the provider is available for the requested date. $available_hours = $this->availability->get_available_hours($date, $service, $provider); - if (count($available_hours) > $max_hours_count) + if (count($available_hours) > $max_hours_count && (empty($hour) || in_array($hour, $available_hours, false))) { $provider_id = $provider['id']; $max_hours_count = count($available_hours); @@ -527,12 +528,13 @@ class Appointments extends EA_Controller { $appointment = $post_data['appointment']; - $date = date('Y-m-d', strtotime($appointment['start_datetime'])); + $appointment_start = new DateTime($appointment['start_datetime']); + $date = $appointment_start->format('Y-m-d'); + $hour = $appointment_start->format('H:i'); if ($appointment['id_users_provider'] === ANY_PROVIDER) { - - $appointment['id_users_provider'] = $this->search_any_provider($date, $appointment['id_services']); + $appointment['id_users_provider'] = $this->search_any_provider($appointment['id_services'], $date, $hour); return $appointment['id_users_provider']; } diff --git a/application/controllers/Backend_api.php b/application/controllers/Backend_api.php index bef6e303..4abc641c 100755 --- a/application/controllers/Backend_api.php +++ b/application/controllers/Backend_api.php @@ -88,6 +88,7 @@ class Backend_api extends EA_Controller { $appointment['service'] = $this->services_model->get_row($appointment['id_services']); $appointment['customer'] = $this->customers_model->get_row($appointment['id_users_customer']); } + unset ($appointment); $user_id = $this->session->userdata('user_id'); $role_slug = $this->session->userdata('role_slug'); diff --git a/application/controllers/Google.php b/application/controllers/Google.php index c84a4846..5cfc5b68 100644 --- a/application/controllers/Google.php +++ b/application/controllers/Google.php @@ -184,7 +184,17 @@ class Google extends EA_Controller { if ($google_event->getStart()->getDateTime() === $google_event->getEnd()->getDateTime()) { - continue; // Skip all day events + $event_start = new DateTime($google_event->getStart()->getDate()); + $event_start->setTimezone($provider_timezone); + $event_end = new DateTime($google_event->getEnd()->getDate()); + $event_end->setTimezone($provider_timezone); + } + else + { + $event_start = new DateTime($google_event->getStart()->getDateTime()); + $event_start->setTimezone($provider_timezone); + $event_end = new DateTime($google_event->getEnd()->getDateTime()); + $event_end->setTimezone($provider_timezone); } $results = $CI->appointments_model->get_batch(['id_google_calendar' => $google_event->getId()]); @@ -194,10 +204,6 @@ class Google extends EA_Controller { continue; } - $event_start = new DateTime($google_event->getStart()->getDateTime()); - $event_start->setTimezone($provider_timezone); - $event_end = new DateTime($google_event->getEnd()->getDateTime()); - $event_end->setTimezone($provider_timezone); // Record doesn't exist in the Easy!Appointments, so add the event now. $appointment = [ diff --git a/application/controllers/api/v1/Availabilities.php b/application/controllers/api/v1/Availabilities.php index 9bf00d1a..7d89d5a0 100644 --- a/application/controllers/api/v1/Availabilities.php +++ b/application/controllers/api/v1/Availabilities.php @@ -14,8 +14,6 @@ require_once __DIR__ . '/API_V1_Controller.php'; require_once __DIR__ . '/../../Appointments.php'; -use EA\Engine\Types\UnsignedInteger; - /** * Availabilities Controller * @@ -32,6 +30,7 @@ class Availabilities extends API_V1_Controller { $this->load->model('providers_model'); $this->load->model('services_model'); $this->load->model('settings_model'); + $this->load->library('availability'); } /** @@ -44,53 +43,22 @@ class Availabilities extends API_V1_Controller { { try { - $provider_id = new UnsignedInteger($this->input->get('providerId')); - $service_id = new UnsignedInteger($this->input->get('serviceId')); + $provider_id = $this->input->get('providerId'); - if ($this->input->get('date')) + $service_id = $this->input->get('serviceId'); + + $date = $this->input->get('date'); + + if ( ! $date) { - $date = new DateTime($this->input->get('date')); - } - else - { - $date = new DateTime(); + $date = date('Y-m-d'); } - $provider = $this->providers_model->get_row($provider_id->get()); - $service = $this->services_model->get_row($service_id->get()); + $provider = $this->providers_model->get_row($provider_id); - $empty_periods = $this->get_provider_available_time_periods($provider_id->get(), - $date->format('Y-m-d'), []); + $service = $this->services_model->get_row($service_id); - $available_hours = $this->calculate_available_hours($empty_periods, - $date->format('Y-m-d'), $service['duration'], FALSE, $service['availabilities_type']); - - if ($service['attendants_number'] > 1) - { - $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 ($date->format('Y-m-d') === date('Y-m-d')) - { - $book_advance_timeout = $this->settings_model->get_setting('book_advance_timeout'); - - foreach ($available_hours as $index => $value) - { - $available_hour = strtotime($value); - $currentHour = strtotime('+' . $book_advance_timeout . ' minutes', strtotime('now')); - if ($available_hour <= $currentHour) - { - unset($available_hours[$index]); - } - } - } - - $available_hours = array_values($available_hours); - sort($available_hours, SORT_STRING); - $available_hours = array_values($available_hours); + $available_hours = $this->availability->get_available_hours($date, $service, $provider); $this->output ->set_content_type('application/json') @@ -98,449 +66,7 @@ class Availabilities extends API_V1_Controller { } catch (Exception $exception) { - exit($this->handle_exception($exception)); + $this->handle_exception($exception); } } - - /** - * 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 The provider's record id. - * @param string $selected_date The date to be checked (MySQL formatted string). - * @param array $exclude_appointments This array contains the ids of the appointments that - * will not be taken into consideration when the available time periods are calculated. - * - * @return array Returns an array with the available time periods of the provider. - */ - protected function get_provider_available_time_periods( - $provider_id, - $selected_date, - $exclude_appointments = [] - ) - { - $this->load->model('appointments_model'); - $this->load->model('providers_model'); - - // Get the provider's working plan and reserved appointments. - $working_plan = json_decode($this->providers_model->get_setting('working_plan', $provider_id), TRUE); - - // Get the provider's working plan exceptions. - $working_plan_exceptions = json_decode($this->providers_model->get_setting('working_plan_exceptions', $provider_id), TRUE); - - $where_clause = [ - 'id_users_provider' => $provider_id - ]; - - $reserved_appointments = $this->appointments_model->get_batch($where_clause); - - // Sometimes it might be necessary to not take into account some appointment records - // in order to display what the providers' available time periods would be without them. - foreach ($exclude_appointments as $excluded_id) - { - foreach ($reserved_appointments as $index => $reserved) - { - if ($reserved['id'] == $excluded_id) - { - unset($reserved_appointments[$index]); - } - } - } - - // Find the empty spaces on the plan. The first split between the plan is due to a break (if exist). After that - // every reserved appointment is considered to be a taken space in the plan. - $selected_date_working_plan = $working_plan[strtolower(date('l', strtotime($selected_date)))]; - - if (isset($working_plan_exceptions[$selected_date])) - { - $selected_date_working_plan = $working_plan_exceptions[$selected_date]; - } - - $available_periods_with_breaks = []; - - if (isset($selected_date_working_plan['breaks'])) - { - $start = new DateTime($selected_date_working_plan['start']); - $end = new DateTime($selected_date_working_plan['end']); - $available_periods_with_breaks[] = [ - 'start' => $selected_date_working_plan['start'], - 'end' => $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 < $start) - { - $break_start = $start; - } - - if ($break_end > $end) - { - $break_end = $end; - } - - if ($break_start >= $break_end) - { - continue; - } - - foreach ($available_periods_with_breaks as $key => $open_period) - { - $s = new DateTime($open_period['start']); - $e = new DateTime($open_period['end']); - - if ($s < $break_end && $break_start < $e) - { // check for overlap - $changed = FALSE; - if ($s < $break_start) - { - $open_start = $s; - $open_end = $break_start; - $available_periods_with_breaks[] = [ - 'start' => $open_start->format('H:i'), - 'end' => $open_end->format('H:i') - ]; - $changed = TRUE; - } - - if ($break_end < $e) - { - $open_start = $break_end; - $open_end = $e; - $available_periods_with_breaks[] = [ - 'start' => $open_start->format('H:i'), - 'end' => $open_end->format('H:i') - ]; - $changed = TRUE; - } - - if ($changed) - { - unset($available_periods_with_breaks[$key]); - } - } - } - } - } - - // Break the empty periods with the reserved appointments. - $available_periods_with_appointments = $available_periods_with_breaks; - - foreach ($reserved_appointments as $appointment) - { - foreach ($available_periods_with_appointments as $index => &$period) - { - $a_start = strtotime($appointment['start_datetime']); - $a_end = strtotime($appointment['end_datetime']); - $p_start = strtotime($selected_date . ' ' . $period['start']); - $p_end = strtotime($selected_date . ' ' . $period['end']); - - if ($a_start <= $p_start && $a_end <= $p_end && $a_end <= $p_start) - { - // The appointment does not belong in this time period, so we - // will not change anything. - } - else - { - if ($a_start <= $p_start && $a_end <= $p_end && $a_end >= $p_start) - { - // The appointment starts before the period and finishes somewhere inside. - // We will need to break this period and leave the available part. - $period['start'] = date('H:i', $a_end); - } - else - { - if ($a_start >= $p_start && $a_end <= $p_end) - { - // The appointment is inside the time period, so we will split the period - // into two new others. - unset($available_periods_with_appointments[$index]); - $available_periods_with_appointments[] = [ - 'start' => date('H:i', $p_start), - 'end' => date('H:i', $a_start) - ]; - $available_periods_with_appointments[] = [ - 'start' => date('H:i', $a_end), - 'end' => date('H:i', $p_end) - ]; - } - else - { - if ($a_start >= $p_start && $a_end >= $p_start && $a_start <= $p_end) - { - // The appointment starts in the period and finishes out of it. We will - // need to remove the time that is taken from the appointment. - $period['end'] = date('H:i', $a_start); - } - else - { - if ($a_start >= $p_start && $a_end >= $p_end && $a_start >= $p_end) - { - // The appointment does not belong in the period so do not change anything. - } - else - { - if ($a_start <= $p_start && $a_end >= $p_end && $a_start <= $p_end) - { - // The appointment is bigger than the period, so this period needs to be removed. - unset($available_periods_with_appointments[$index]); - } - } - } - } - } - } - } - } - - return array_values($available_periods_with_appointments); - } - - /** - * 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 - * "_getProviderAvailableTimePeriods" 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 bool $manage_mode (optional) Whether we are currently on manage mode (editing an existing appointment). - * @param string $availabilities_type Optional ('flexible'), the service availabilities type. - * - * @return array Returns an array with the available hours for the appointment. - */ - protected function calculate_available_hours( - array $empty_periods, - $selected_date, - $service_duration, - $manage_mode = FALSE, - $availabilities_type = 'flexible' - ) - { - $this->load->model('settings_model'); - - $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. - */ - protected function get_multiple_attendants_hours( - $selected_date, - $service, - $provider - ) - { - $this->load->model('appointments_model'); - $this->load->model('services_model'); - $this->load->model('providers_model'); - - $unavailabilities = $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_unavailabilities($periods, $unavailabilities); - - $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. - */ - 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 unavailabilities from the available time periods of the selected date. - * - * @param array $periods Available time periods. - * @param array $unavailabilities Unavailabilities of the current date. - * - * @return array Returns the available time periods without the unavailabilities. - */ - public function remove_unavailabilities($periods, $unavailabilities) - { - foreach ($unavailabilities as $unavailability) - { - $unavailability_start = new DateTime($unavailability['start_datetime']); - $unavailability_end = new DateTime($unavailability['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) - { - // Unavaibility contains period - $period['start'] = $unavailability_end; - continue; - } - } - } - - return $periods; - } } diff --git a/application/core/EA_Model.php b/application/core/EA_Model.php index 3e66a3e7..58c1ffc8 100644 --- a/application/core/EA_Model.php +++ b/application/core/EA_Model.php @@ -58,5 +58,11 @@ * @property Timezones $timezones */ class EA_Model extends CI_Model { - // + /** + * EA_Model constructor. + */ + public function __construct() + { + // + } } diff --git a/application/helpers/google_analytics_helper.php b/application/helpers/google_analytics_helper.php index 6d26abe1..8229c4eb 100644 --- a/application/helpers/google_analytics_helper.php +++ b/application/helpers/google_analytics_helper.php @@ -26,9 +26,12 @@ function google_analytics_script() $google_analytics_code = $CI->settings_model->get_setting('google_analytics_code'); - if ($google_analytics_code !== '') - { - echo ' + if ($google_analytics_code !== '') { + + // If the google analytics code starts with UA then it is a Universal Analytics Property and the script stays + // the legacy one + if (substr($google_analytics_code, 0, 2) === "UA") { + echo ' '; + } + + // If the google analytics code starts with a G then it is a Google Analytics 4-Property and the script + // to inject it looks different. + if (substr($google_analytics_code, 0, 2) === "G-") { + echo ' + + + '; + } } } diff --git a/application/language/arabic/calendar_lang.php b/application/language/arabic/calendar_lang.php new file mode 100644 index 00000000..ce838142 --- /dev/null +++ b/application/language/arabic/calendar_lang.php @@ -0,0 +1,84 @@ + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/language/catalan/migration_lang.php b/application/language/catalan/migration_lang.php new file mode 100644 index 00000000..62ce1526 --- /dev/null +++ b/application/language/catalan/migration_lang.php @@ -0,0 +1,20 @@ + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/language/persian/migration_lang.php b/application/language/persian/migration_lang.php new file mode 100644 index 00000000..0f826c0f --- /dev/null +++ b/application/language/persian/migration_lang.php @@ -0,0 +1,20 @@ +diff($end_hour); - while (($diff->h * 60 + $diff->i) >= (int)$service['duration']) + while (($diff->h * 60 + $diff->i) >= (int)$service['duration'] && $diff->invert === 0) { $available_hours[] = $current_hour->format('H:i'); $current_hour->add(new DateInterval('PT' . $interval . 'M')); diff --git a/application/libraries/Notifications.php b/application/libraries/Notifications.php index 2188f022..86ac282e 100644 --- a/application/libraries/Notifications.php +++ b/application/libraries/Notifications.php @@ -130,7 +130,7 @@ class Notifications { continue; } - if (in_array($provider['id'], $secretary['providers'])) + if (!in_array($provider['id'], $secretary['providers'])) { continue; } @@ -189,7 +189,7 @@ class Notifications { foreach ($admins as $admin) { - if ( ! $admin['settings']['notifications'] === '0') + if ($admin['settings']['notifications'] === '0') { continue; } @@ -204,12 +204,12 @@ class Notifications { foreach ($secretaries as $secretary) { - if ( ! $secretary['settings']['notifications'] === '0') + if ($secretary['settings']['notifications'] === '0') { continue; } - if (in_array($provider['id'], $secretary['providers'])) + if (!in_array($provider['id'], $secretary['providers'])) { continue; } diff --git a/application/libraries/Timezones.php b/application/libraries/Timezones.php index 357bca0b..9df4a4c9 100644 --- a/application/libraries/Timezones.php +++ b/application/libraries/Timezones.php @@ -458,6 +458,44 @@ class Timezones { 'Australia/LHI' => 'LHI (+10:30)', 'Australia/Lord_Howe' => 'Lord_Howe (+10:30)', ], + 'Pacific' => [ + 'Pacific/Apia' => 'Apia (+13:00)', + 'Pacific/Auckland' => 'Auckland (+12:00)', + 'Pacific/Bougainville' => 'Bougainville (+11:00)', + 'Pacific/Chatham' => 'Chatham (+12:45)', + 'Pacific/Chuuk' => 'Chuuk (+10:00)', + 'Pacific/Easter' => 'Easter (−06:00)', + 'Pacific/Efate' => 'Efate (+11:00)', + 'Pacific/Enderbury' => 'Enderbury (+13:00)', + 'Pacific/Fakaofo' => 'Fakaofo (+13:00)', + 'Pacific/Fiji' => 'Fiji (+12:00)', + 'Pacific/Funafuti' => 'Funafuti (+12:00)', + 'Pacific/Galapagos' => 'Galapagos (−06:00)', + 'Pacific/Gambier' => 'Gambier (−09:00)', + 'Pacific/Guadalcanal' => 'Guadalcanal (+11:00)', + 'Pacific/Guam' => 'Guam (+10:00)', + 'Pacific/Honolulu' => 'Honolulu (−10:00)', + 'Pacific/Kiritimati' => 'Kiritimati (+14:00)', + 'Pacific/Kosrae' => 'Kosrae (+11:00)', + 'Pacific/Kwajalein' => 'Kwajalein (+12:00)', + 'Pacific/Majuro' => 'Majuro (+12:00)', + 'Pacific/Marquesas' => 'Marquesas (−09:30)', + 'Pacific/Nauru' => 'Nauru (+12:00)', + 'Pacific/Niue' => 'Niue (−11:00)', + 'Pacific/Norfolk' => 'Norfolk (+11:00)', + 'Pacific/Noumea' => 'Noumea (+11:00)', + 'Pacific/Pago_Pago' => 'Pago_Pago (−11:00)', + 'Pacific/Palau' => 'Palau (+09:00)', + 'Pacific/Pitcairn' => 'Pitcairn (−08:00)', + 'Pacific/Pohnpei' => 'Pohnpei (+11:00)', + 'Pacific/Port_Moresby' => 'Port_Moresby (+10:00)', + 'Pacific/Rarotonga' => 'Rarotonga (−10:00)', + 'Pacific/Tahiti' => 'Tahiti (−10:00)', + 'Pacific/Tarawa' => 'Tarawa (+12:00)', + 'Pacific/Tongatapu' => 'Tongatapu (+13:00)', + 'Pacific/Wake' => 'Wake (+12:00)', + 'Pacific/Wallis' => 'Wallis (+12:00)', + ], ]; /** diff --git a/application/migrations/001_specific_calendar_sync.php b/application/migrations/001_specific_calendar_sync.php index 8b701146..ae0a8076 100644 --- a/application/migrations/001_specific_calendar_sync.php +++ b/application/migrations/001_specific_calendar_sync.php @@ -469,7 +469,7 @@ class Migration_Specific_calendar_sync extends CI_Migration { $this->db->insert('settings', [ 'name' => 'company_working_plan', - 'value' => '{"monday":{"start":"09:00","end":"18:00","breaks":[{"start":"11:20","end":"11:30"},{"start":"14:30","end":"15:00"}]},"tuesday":{"start":"09:00","end":"18:00","breaks":[{"start":"11:20","end":"11:30"},{"start":"14:30","end":"15:00"}]},"wednesday":{"start":"09:00","end":"18:00","breaks":[{"start":"11:20","end":"11:30"},{"start":"14:30","end":"15:00"}]},"thursday":{"start":"09:00","end":"18:00","breaks":[{"start":"11:20","end":"11:30"},{"start":"14:30","end":"15:00"}]},"friday":{"start":"09:00","end":"18:00","breaks":[{"start":"11:20","end":"11:30"},{"start":"14:30","end":"15:00"}]},"saturday":{"start":"09:00","end":"18:00","breaks":[{"start":"11:20","end":"11:30"},{"start":"14:30","end":"15:00"}]},"sunday":{"start":"09:00","end":"18:00","breaks":[{"start":"11:20","end":"11:30"},{"start":"14:30","end":"15:00"}]}}' + 'value' => '{"monday":{"start":"09:00","end":"18:00","breaks":[{"start":"14:30","end":"15:00"}]},"tuesday":{"start":"09:00","end":"18:00","breaks":[{"start":"14:30","end":"15:00"}]},"wednesday":{"start":"09:00","end":"18:00","breaks":[{"start":"14:30","end":"15:00"}]},"thursday":{"start":"09:00","end":"18:00","breaks":[{"start":"14:30","end":"15:00"}]},"friday":{"start":"09:00","end":"18:00","breaks":[{"start":"14:30","end":"15:00"}]},"saturday":{"start":"09:00","end":"18:00","breaks":[{"start":"14:30","end":"15:00"}]},"sunday":{"start":"09:00","end":"18:00","breaks":[{"start":"14:30","end":"15:00"}]}}' ]); $this->db->insert('settings', [ diff --git a/application/models/Appointments_model.php b/application/models/Appointments_model.php index 9866d350..a757636c 100644 --- a/application/models/Appointments_model.php +++ b/application/models/Appointments_model.php @@ -24,6 +24,7 @@ class Appointments_model extends EA_Model { { parent::__construct(); $this->load->helper('data_validation'); + $this->load->helper('string'); } /** @@ -153,7 +154,7 @@ class Appointments_model extends EA_Model { protected function insert($appointment) { $appointment['book_datetime'] = date('Y-m-d H:i:s'); - $appointment['hash'] = $this->generate_hash(); + $appointment['hash'] = random_string('alnum', 12); if ( ! $this->db->insert('appointments', $appointment)) { @@ -163,20 +164,6 @@ class Appointments_model extends EA_Model { return (int)$this->db->insert_id(); } - /** - * Generate a unique hash for the given appointment data. - * - * This method uses the current date-time to generate a unique hash string that is later used to identify this - * appointment. Hash is needed when the email is send to the user with an edit link. - * - * @return string Returns the unique appointment hash. - */ - public function generate_hash() - { - $current_date = new DateTime(); - return md5($current_date->getTimestamp()); - } - /** * Update an existing appointment record in the database. * diff --git a/application/models/Services_model.php b/application/models/Services_model.php index c56ccbd7..140f8504 100644 --- a/application/models/Services_model.php +++ b/application/models/Services_model.php @@ -97,6 +97,11 @@ class Services_model extends EA_Model { { throw new Exception('Service duration is not numeric.'); } + + if ((int)$service['duration'] < EVENT_MINIMUM_DURATION) + { + throw new Exception('The service duration cannot be less than ' . EVENT_MINIMUM_DURATION . ' minutes.'); + } } if ($service['price'] !== NULL) diff --git a/application/views/backend/header.php b/application/views/backend/header.php index 789e8c10..1743c283 100755 --- a/application/views/backend/header.php +++ b/application/views/backend/header.php @@ -27,6 +27,7 @@ + diff --git a/application/views/backend/services.php b/application/views/backend/services.php index 7aeca7fe..444d2c6d 100755 --- a/application/views/backend/services.php +++ b/application/views/backend/services.php @@ -116,7 +116,7 @@ * - +
diff --git a/application/views/backend/settings.php b/application/views/backend/settings.php index 044caaef..f3b758ad 100755 --- a/application/views/backend/settings.php +++ b/application/views/backend/settings.php @@ -149,7 +149,7 @@
- diff --git a/assets/css/backend.css b/assets/css/backend.css index 7351c02b..7012be4b 100644 --- a/assets/css/backend.css +++ b/assets/css/backend.css @@ -427,6 +427,7 @@ body legend { #calendar .fc-event { border: 1px solid #4790CA; background-color: #4790CA; + min-height: 12px; } #existing-customers-list { @@ -708,7 +709,7 @@ body .form-horizontal .controls { #users-page #providers .working-plan-view .work-start, #users-page #providers .working-plan-view .work-end { - max-width: 88px; + max-width: 100px; } #users-page .btn-toolbar { @@ -720,6 +721,11 @@ body .form-horizontal .controls { clear: both; } +#users-page #providers .breaks.table, +#settings-page .breaks.table { + table-layout: fixed; +} + #users-page #providers .breaks .btn { margin-right: 5px; padding: 4px 7px; @@ -784,12 +790,12 @@ body .form-horizontal .controls { } #business-logic .working-plan td input[type="text"] { - max-width: 88px; + max-width: 100px; } #business-logic .working-plan .work-start, #business-logic .working-plan .work-end { - max-width: 88px; + max-width: 100px; } #business-logic .working-plan label.checkbox { diff --git a/assets/ext/jquery-ui/jquery-ui.touch-punch.min.js b/assets/ext/jquery-ui/jquery-ui.touch-punch.min.js new file mode 100644 index 00000000..31272ce6 --- /dev/null +++ b/assets/ext/jquery-ui/jquery-ui.touch-punch.min.js @@ -0,0 +1,11 @@ +/*! + * jQuery UI Touch Punch 0.2.3 + * + * Copyright 2011–2014, Dave Furfero + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Depends: + * jquery.ui.widget.js + * jquery.ui.mouse.js + */ +!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery); \ No newline at end of file diff --git a/assets/js/backend_calendar_default_view.js b/assets/js/backend_calendar_default_view.js index 63817f5e..9f9275be 100755 --- a/assets/js/backend_calendar_default_view.js +++ b/assets/js/backend_calendar_default_view.js @@ -447,7 +447,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {}; 'text': EALang.start }), $('', { - 'text': GeneralFunctions.formatDate(event.start.format('YYYY-MM-DD HH:mm:ss'), GlobalVariables.dateFormat, true) + 'text': GeneralFunctions.formatDate(event.data.date + ' ' + event.data.workingPlanException.start, GlobalVariables.dateFormat, true) }), $('
'), @@ -456,7 +456,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {}; 'text': EALang.end }), $('', { - 'text': GeneralFunctions.formatDate(event.end.format('YYYY-MM-DD HH:mm:ss'), GlobalVariables.dateFormat, true) + 'text': GeneralFunctions.formatDate(event.data.date + ' ' + event.data.workingPlanException.end, GlobalVariables.dateFormat, true) }), $('
'), @@ -475,7 +475,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {}; 'class': 'd-flex justify-content-between', 'html': [ $('