Merge branch 'develop'

This commit is contained in:
Alex Tselegidis 2020-10-22 12:52:47 +03:00
commit 6bf4e8b4ea
49 changed files with 10866 additions and 1330 deletions

View file

@ -9,7 +9,7 @@
| |
*/ */
$config['version'] = '1.4.0'; // This must be changed manually. $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; $config['debug'] = Config::DEBUG_MODE;
/* /*
@ -302,7 +302,7 @@ $config['cache_path'] = __DIR__ . '/../../storage/cache/';
| new release. | new release.
| |
*/ */
$config['cache_busting_token'] = '93GX4'; $config['cache_busting_token'] = '824HX';
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,8 @@ use EA\Engine\Types\Url;
* @property Customers_Model $customers_model * @property Customers_Model $customers_model
* @property Settings_Model $settings_model * @property Settings_Model $settings_model
* @property Timezones $timezones * @property Timezones $timezones
* @property Synchronization $synchronization
* @property Notifications $notifications
* @property Roles_Model $roles_model * @property Roles_Model $roles_model
* @property Secretaries_Model $secretaries_model * @property Secretaries_Model $secretaries_model
* @property Admins_Model $admins_model * @property Admins_Model $admins_model
@ -102,7 +104,7 @@ class Backend_api extends CI_Controller {
'start_datetime >=' => $startDate, 'start_datetime >=' => $startDate,
'end_datetime <=' => $endDate 'end_datetime <=' => $endDate
]), ]),
'unavailabilities' => $this->appointments_model->get_batch([ 'unavailability_events' => $this->appointments_model->get_batch([
'is_unavailable' => TRUE, 'is_unavailable' => TRUE,
'start_datetime >=' => $startDate, 'start_datetime >=' => $startDate,
'end_datetime <=' => $endDate '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('customers_model');
$this->load->model('settings_model'); $this->load->model('settings_model');
$this->load->library('timezones'); $this->load->library('timezones');
$this->load->library('synchronization');
$this->load->library('notifications');
$this->load->model('user_model'); $this->load->model('user_model');
// Save customer changes to the database. // 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); $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]['add']
: $this->privileges[PRIV_CUSTOMERS]['edit']; : $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.'); 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); $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]['add']
: $this->privileges[PRIV_APPOINTMENTS]['edit']; : $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.'); throw new Exception('You do not have the required privileges for this task.');
} }
$manage_mode = isset($appointment['id']); $manage_mode = isset($appointment['id']);
// If the appointment does not contain the customer record id, then it // If the appointment does not contain the customer record id, then it means that is is going to be
// means that is is going to be inserted. Get the customer's record id. // inserted. Get the customer's record ID.
if ( ! isset($appointment['id_users_customer'])) if ( ! isset($appointment['id_users_customer']))
{ {
$appointment['id_users_customer'] = $customer['id']; $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') 'time_format' => $this->settings_model->get_setting('time_format')
]; ];
// Sync appointment changes with Google Calendar. $this->synchronization->sync_appointment_deleted($appointment, $provider);
try $this->notifications->notify_appointment_deleted($appointment, $service, $provider, $customer, $settings);
{
$google_sync = $this->providers_model->get_setting('google_sync',
$appointment['id_users_provider']);
if ($google_sync == TRUE) $response = AJAX_SUCCESS;
{
$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];
}
} }
catch (Exception $exception) catch (Exception $exception)
{ {
@ -1032,10 +907,10 @@ class Backend_api extends CI_Controller {
$this->load->model('customers_model'); $this->load->model('customers_model');
$customer = json_decode($this->input->post('customer'), TRUE); $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]['add']
: $this->privileges[PRIV_CUSTOMERS]['edit']; : $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.'); 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'); $this->load->model('services_model');
$service = json_decode($this->input->post('service'), TRUE); $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]['add']
: $this->privileges[PRIV_SERVICES]['edit']; : $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.'); 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'); $this->load->model('services_model');
$category = json_decode($this->input->post('category'), TRUE); $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]['add']
: $this->privileges[PRIV_SERVICES]['edit']; : $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.'); throw new Exception('You do not have the required privileges for this task.');
} }

View file

@ -62,7 +62,7 @@ class API_V1_Controller extends CI_Controller {
} }
catch (Exception $exception) 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. * @param Exception $exception Thrown exception to be outputted.
*/ */
protected function _handleException(Exception $exception) protected function handle_exception(Exception $exception)
{ {
$error = [ $error = [
'code' => $exception->getCode() ?: 500, 'code' => $exception->getCode() ?: 500,
@ -166,7 +166,7 @@ class API_V1_Controller extends CI_Controller {
* *
* @throws \EA\Engine\Api\V1\Exception * @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'); throw new \EA\Engine\Api\V1\Exception('The requested record was not found!', 404, 'Not Found');
} }

View file

@ -76,7 +76,7 @@ class Admins extends API_V1_Controller {
if ($id !== NULL && count($admins) === 0) if ($id !== NULL && count($admins) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$response = new Response($admins); $response = new Response($admins);
@ -92,7 +92,7 @@ class Admins extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -123,7 +123,7 @@ class Admins extends API_V1_Controller {
} }
catch (Exception $exception) 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) if ($id !== NULL && count($batch) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$request = new Request(); $request = new Request();
@ -158,7 +158,7 @@ class Admins extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -182,7 +182,7 @@ class Admins extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
} }

View file

@ -37,6 +37,8 @@ use EA\Engine\Types\NonEmptyText;
* @property Customers_Model $customers_model * @property Customers_Model $customers_model
* @property Settings_Model $settings_model * @property Settings_Model $settings_model
* @property Timezones $timezones * @property Timezones $timezones
* @property Notifications $notifications
* @property Synchronization $synchronization
* @property Roles_Model $roles_model * @property Roles_Model $roles_model
* @property Secretaries_Model $secretaries_model * @property Secretaries_Model $secretaries_model
* @property Admins_Model $admins_model * @property Admins_Model $admins_model
@ -59,6 +61,12 @@ class Appointments extends API_V1_Controller {
{ {
parent::__construct(); parent::__construct();
$this->load->model('appointments_model'); $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; $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) if ($id !== NULL && count($appointments) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$response = new Response($appointments); $response = new Response($appointments);
@ -100,7 +108,7 @@ class Appointments extends API_V1_Controller {
} }
catch (Exception $exception) 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() public function post()
{ {
$this->load->model('services_model');
try try
{ {
// Insert the appointment to the database. // Insert the appointment to the database.
@ -138,6 +144,20 @@ class Appointments extends API_V1_Controller {
$id = $this->appointments_model->add($appointment); $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. // Fetch the new object from the database and return it to the client.
$batch = $this->appointments_model->get_batch('id = ' . $id); $batch = $this->appointments_model->get_batch('id = ' . $id);
$response = new Response($batch); $response = new Response($batch);
@ -146,7 +166,7 @@ class Appointments extends API_V1_Controller {
} }
catch (Exception $exception) 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) if ($id !== NULL && count($batch) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$request = new Request(); $request = new Request();
$updatedAppointment = $request->getBody(); $updated_appointment = $request->getBody();
$baseAppointment = $batch[0]; $base_appointment = $batch[0];
$this->parser->decode($updatedAppointment, $baseAppointment); $this->parser->decode($updated_appointment, $base_appointment);
$updatedAppointment['id'] = $id; $updated_appointment['id'] = $id;
$id = $this->appointments_model->add($updatedAppointment); $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. // Fetch the updated object from the database and return it to the client.
$batch = $this->appointments_model->get_batch('id = ' . $id); $batch = $this->appointments_model->get_batch('id = ' . $id);
@ -181,7 +216,7 @@ class Appointments extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
exit($this->_handleException($exception)); exit($this->handle_exception($exception));
} }
} }
@ -194,8 +229,23 @@ class Appointments extends API_V1_Controller {
{ {
try 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->appointments_model->delete($id);
$this->synchronization->sync_appointment_deleted($appointment, $provider);
$this->notifications->notify_appointment_deleted($appointment, $service, $provider, $customer, $settings);
$response = new Response([ $response = new Response([
'code' => 200, 'code' => 200,
'message' => 'Record was deleted successfully!' 'message' => 'Record was deleted successfully!'
@ -205,7 +255,7 @@ class Appointments extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
exit($this->_handleException($exception)); exit($this->handle_exception($exception));
} }
} }
} }

View file

@ -66,8 +66,8 @@ class Availabilities extends API_V1_Controller {
{ {
try try
{ {
$providerId = new UnsignedInteger($this->input->get('providerId')); $provider_id = new UnsignedInteger($this->input->get('providerId'));
$serviceId = new UnsignedInteger($this->input->get('serviceId')); $service_id = new UnsignedInteger($this->input->get('serviceId'));
if ($this->input->get('date')) if ($this->input->get('date'))
{ {
@ -78,49 +78,49 @@ class Availabilities extends API_V1_Controller {
$date = new DateTime(); $date = new DateTime();
} }
$provider = $this->providers_model->get_row($providerId->get()); $provider = $this->providers_model->get_row($provider_id->get());
$service = $this->services_model->get_row($serviceId->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'), []); $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']); $date->format('Y-m-d'), $service['duration'], FALSE, $service['availabilities_type']);
if ($service['attendants_number'] > 1) 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 // If the selected date is today, remove past hours. It is important include the timeout before booking
// booking that is set in the back-office the system. Normally we might want the customer to book // that is set in the back-office the system. Normally we might want the customer to book an appointment
// an appointment that is at least half or one hour from now. The setting is stored in minutes. // 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')) 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); $available_hour = strtotime($value);
$currentHour = strtotime('+' . $bookAdvanceTimeout . ' minutes', strtotime('now')); $currentHour = strtotime('+' . $book_advance_timeout . ' minutes', strtotime('now'));
if ($availableHour <= $currentHour) if ($available_hour <= $currentHour)
{ {
unset($availableHours[$index]); unset($available_hours[$index]);
} }
} }
} }
$availableHours = array_values($availableHours); $available_hours = array_values($available_hours);
sort($availableHours, SORT_STRING); sort($available_hours, SORT_STRING);
$availableHours = array_values($availableHours); $available_hours = array_values($available_hours);
$this->output $this->output
->set_content_type('application/json') ->set_content_type('application/json')
->set_output(json_encode($availableHours)); ->set_output(json_encode($available_hours));
} }
catch (Exception $exception) 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. * @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, $provider_id,
$selected_date, $selected_date,
$exclude_appointments = [] $exclude_appointments = []
@ -342,7 +342,7 @@ class Availabilities extends API_V1_Controller {
* *
* @return array Returns an array with the available hours for the appointment. * @return array Returns an array with the available hours for the appointment.
*/ */
protected function _calculateAvailableHours( protected function calculate_available_hours(
array $empty_periods, array $empty_periods,
$selected_date, $selected_date,
$service_duration, $service_duration,
@ -385,7 +385,7 @@ class Availabilities extends API_V1_Controller {
* *
* @return array Returns the available hours array. * @return array Returns the available hours array.
*/ */
protected function _getMultipleAttendantsHours( protected function get_multiple_attendants_hours(
$selected_date, $selected_date,
$service, $service,
$provider $provider
@ -412,8 +412,8 @@ class Availabilities extends API_V1_Controller {
] ]
]; ];
$periods = $this->_removeBreaks($selected_date, $periods, $working_hours['breaks']); $periods = $this->remove_breaks($selected_date, $periods, $working_hours['breaks']);
$periods = $this->_removeUnavailabilities($periods, $unavailabilities); $periods = $this->remove_unavailabilities($periods, $unavailabilities);
$hours = []; $hours = [];
@ -455,7 +455,7 @@ class Availabilities extends API_V1_Controller {
* *
* @return array Returns the available time periods without the breaks. * @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) if ( ! $breaks)
{ {
@ -517,7 +517,7 @@ class Availabilities extends API_V1_Controller {
* *
* @return array Returns the available time periods without the unavailabilities. * @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) foreach ($unavailabilities as $unavailability)
{ {

View file

@ -76,7 +76,7 @@ class Categories extends API_V1_Controller {
if ($id !== NULL && count($categories) === 0) if ($id !== NULL && count($categories) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$response = new Response($categories); $response = new Response($categories);
@ -92,7 +92,7 @@ class Categories extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -123,7 +123,7 @@ class Categories extends API_V1_Controller {
} }
catch (Exception $exception) 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) if ($id !== NULL && count($batch) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$request = new Request(); $request = new Request();
$updatedCategory = $request->getBody(); $updated_category = $request->getBody();
$baseCategory = $batch[0]; $base_category = $batch[0];
$this->parser->decode($updatedCategory, $baseCategory); $this->parser->decode($updated_category, $base_category);
$updatedCategory['id'] = $id; $updated_category['id'] = $id;
$id = $this->services_model->add_category($updatedCategory); $id = $this->services_model->add_category($updated_category);
// Fetch the updated object from the database and return it to the client. // Fetch the updated object from the database and return it to the client.
$batch = $this->services_model->get_all_categories('id = ' . $id); $batch = $this->services_model->get_all_categories('id = ' . $id);
@ -158,7 +158,7 @@ class Categories extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -182,7 +182,7 @@ class Categories extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
} }

View file

@ -76,7 +76,7 @@ class Customers extends API_V1_Controller {
if ($id !== NULL && count($customers) === 0) if ($id !== NULL && count($customers) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$response = new Response($customers); $response = new Response($customers);
@ -92,7 +92,7 @@ class Customers extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -123,7 +123,7 @@ class Customers extends API_V1_Controller {
} }
catch (Exception $exception) 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) if ($id !== NULL && count($batch) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$request = new Request(); $request = new Request();
$updatedCustomer = $request->getBody(); $updated_customer = $request->getBody();
$baseCustomer = $batch[0]; $base_customer = $batch[0];
$this->parser->decode($updatedCustomer, $baseCustomer); $this->parser->decode($updated_customer, $base_customer);
$updatedCustomer['id'] = $id; $updated_customer['id'] = $id;
$id = $this->customers_model->add($updatedCustomer); $id = $this->customers_model->add($updated_customer);
// Fetch the updated object from the database and return it to the client. // Fetch the updated object from the database and return it to the client.
$batch = $this->customers_model->get_batch('id = ' . $id); $batch = $this->customers_model->get_batch('id = ' . $id);
@ -158,7 +158,7 @@ class Customers extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -182,7 +182,7 @@ class Customers extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
} }

View file

@ -76,7 +76,7 @@ class Providers extends API_V1_Controller {
if ($id !== NULL && count($providers) === 0) if ($id !== NULL && count($providers) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$response = new Response($providers); $response = new Response($providers);
@ -92,7 +92,7 @@ class Providers extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -123,7 +123,7 @@ class Providers extends API_V1_Controller {
} }
catch (Exception $exception) 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) if ($id !== NULL && count($batch) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$request = new Request(); $request = new Request();
$updatedProvider = $request->getBody(); $updated_provider = $request->getBody();
$baseProvider = $batch[0]; $base_provider = $batch[0];
$this->parser->decode($updatedProvider, $baseProvider); $this->parser->decode($updated_provider, $base_provider);
$updatedProvider['id'] = $id; $updated_provider['id'] = $id;
$id = $this->providers_model->add($updatedProvider); $id = $this->providers_model->add($updated_provider);
// Fetch the updated object from the database and return it to the client. // Fetch the updated object from the database and return it to the client.
$batch = $this->providers_model->get_batch('id = ' . $id); $batch = $this->providers_model->get_batch('id = ' . $id);
@ -158,7 +158,7 @@ class Providers extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -182,7 +182,7 @@ class Providers extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
} }

View file

@ -76,7 +76,7 @@ class Secretaries extends API_V1_Controller {
if ($id !== NULL && count($secretaries) === 0) if ($id !== NULL && count($secretaries) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$response = new Response($secretaries); $response = new Response($secretaries);
@ -92,7 +92,7 @@ class Secretaries extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -123,7 +123,7 @@ class Secretaries extends API_V1_Controller {
} }
catch (Exception $exception) 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) if ($id !== NULL && count($batch) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$request = new Request(); $request = new Request();
$updatedSecretary = $request->getBody(); $updated_secretary = $request->getBody();
$baseSecretary = $batch[0]; $base_secretary = $batch[0];
$this->parser->decode($updatedSecretary, $baseSecretary); $this->parser->decode($updated_secretary, $base_secretary);
$updatedSecretary['id'] = $id; $updated_secretary['id'] = $id;
$id = $this->secretaries_model->add($updatedSecretary); $id = $this->secretaries_model->add($updated_secretary);
// Fetch the updated object from the database and return it to the client. // Fetch the updated object from the database and return it to the client.
$batch = $this->secretaries_model->get_batch('id = ' . $id); $batch = $this->secretaries_model->get_batch('id = ' . $id);
@ -158,7 +158,7 @@ class Secretaries extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -182,7 +182,7 @@ class Secretaries extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
} }

View file

@ -76,7 +76,7 @@ class Services extends API_V1_Controller {
if ($id !== NULL && count($services) === 0) if ($id !== NULL && count($services) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$response = new Response($services); $response = new Response($services);
@ -92,7 +92,7 @@ class Services extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -123,7 +123,7 @@ class Services extends API_V1_Controller {
} }
catch (Exception $exception) 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) if ($id !== NULL && count($batch) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$request = new Request(); $request = new Request();
$updatedService = $request->getBody(); $updated_service = $request->getBody();
$baseService = $batch[0]; $base_service = $batch[0];
$this->parser->decode($updatedService, $baseService); $this->parser->decode($updated_service, $base_service);
$updatedService['id'] = $id; $updated_service['id'] = $id;
$id = $this->services_model->add($updatedService); $id = $this->services_model->add($updated_service);
// Fetch the updated object from the database and return it to the client. // Fetch the updated object from the database and return it to the client.
$batch = $this->services_model->get_batch('id = ' . $id); $batch = $this->services_model->get_batch('id = ' . $id);
@ -158,7 +158,7 @@ class Services extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
@ -182,7 +182,7 @@ class Services extends API_V1_Controller {
} }
catch (Exception $exception) catch (Exception $exception)
{ {
$this->_handleException($exception); $this->handle_exception($exception);
} }
} }
} }

View file

@ -87,7 +87,7 @@ class Settings extends API_V1_Controller {
if (empty($setting)) if (empty($setting))
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
unset($setting['id']); unset($setting['id']);
@ -110,7 +110,7 @@ class Settings extends API_V1_Controller {
} }
catch (Exception $exception) 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) 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) catch (Exception $exception)
{ {
exit($this->_handleException($exception)); exit($this->handle_exception($exception));
} }
} }
} }

View file

@ -76,7 +76,7 @@ class Unavailabilities extends API_V1_Controller {
if ($id !== NULL && count($unavailabilities) === 0) if ($id !== NULL && count($unavailabilities) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$response = new Response($unavailabilities); $response = new Response($unavailabilities);
@ -92,7 +92,7 @@ class Unavailabilities extends API_V1_Controller {
} }
catch (Exception $exception) 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) 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) if ($id !== NULL && count($batch) === 0)
{ {
$this->_throwRecordNotFound(); $this->throw_record_not_found();
} }
$request = new Request(); $request = new Request();
@ -158,7 +158,7 @@ class Unavailabilities extends API_V1_Controller {
} }
catch (Exception $exception) 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) catch (Exception $exception)
{ {
exit($this->_handleException($exception)); exit($this->handle_exception($exception));
} }
} }
} }

View file

@ -0,0 +1,552 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @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);
}
}

View file

@ -0,0 +1,227 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @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;
}
}
}

View file

@ -0,0 +1,120 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @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;
}
}
}
}

View file

@ -10,7 +10,6 @@
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/bootstrap/css/bootstrap.min.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/bootstrap/css/bootstrap.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/jquery-ui/jquery-ui.min.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/jquery-ui/jquery-ui.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/jquery-qtip/jquery.qtip.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/cookieconsent/cookieconsent.min.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/cookieconsent/cookieconsent.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/css/frontend.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/css/frontend.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/css/general.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/css/general.css') ?>">
@ -32,17 +31,17 @@
<span id="company-name"><?= $company_name ?></span> <span id="company-name"><?= $company_name ?></span>
<div id="steps"> <div id="steps">
<div id="step-1" class="book-step active-step" title="<?= lang('service_and_provider') ?>"> <div id="step-1" class="book-step active-step" data-tippy-content="<?= lang('service_and_provider') ?>">
<strong>1</strong> <strong>1</strong>
</div> </div>
<div id="step-2" class="book-step" title="<?= lang('appointment_date_and_time') ?>"> <div id="step-2" class="book-step" data-toggle="tooltip" data-tippy-content="<?= lang('appointment_date_and_time') ?>">
<strong>2</strong> <strong>2</strong>
</div> </div>
<div id="step-3" class="book-step" title="<?= lang('customer_information') ?>"> <div id="step-3" class="book-step" data-toggle="tooltip" data-tippy-content="<?= lang('customer_information') ?>">
<strong>3</strong> <strong>3</strong>
</div> </div>
<div id="step-4" class="book-step" title="<?= lang('appointment_confirmation') ?>"> <div id="step-4" class="book-step" data-toggle="tooltip" data-tippy-content="<?= lang('appointment_confirmation') ?>">
<strong>4</strong> <strong>4</strong>
</div> </div>
</div> </div>
@ -450,9 +449,10 @@
<script src="<?= asset_url('assets/js/general_functions.js') ?>"></script> <script src="<?= asset_url('assets/js/general_functions.js') ?>"></script>
<script src="<?= asset_url('assets/ext/jquery/jquery.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/jquery/jquery.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/jquery-ui/jquery-ui.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/jquery-ui/jquery-ui.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/jquery-qtip/jquery.qtip.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/cookieconsent/cookieconsent.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/cookieconsent/cookieconsent.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/bootstrap/js/bootstrap.bundle.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/bootstrap/js/bootstrap.bundle.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/popper/popper.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/tippy/tippy-bundle.umd.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/datejs/date.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/datejs/date.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/moment/moment.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/moment/moment.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/moment/moment-timezone-with-data.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/moment/moment-timezone-with-data.min.js') ?>"></script>

View file

@ -9,7 +9,7 @@
<script src="<?= asset_url('assets/js/backend_calendar_table_view.js') ?>"></script> <script src="<?= asset_url('assets/js/backend_calendar_table_view.js') ?>"></script>
<script src="<?= asset_url('assets/js/backend_calendar_google_sync.js') ?>"></script> <script src="<?= asset_url('assets/js/backend_calendar_google_sync.js') ?>"></script>
<script src="<?= asset_url('assets/js/backend_calendar_appointments_modal.js') ?>"></script> <script src="<?= asset_url('assets/js/backend_calendar_appointments_modal.js') ?>"></script>
<script src="<?= asset_url('assets/js/backend_calendar_unavailabilities_modal.js') ?>"></script> <script src="<?= asset_url('assets/js/backend_calendar_unavailability_events_modal.js') ?>"></script>
<script src="<?= asset_url('assets/js/backend_calendar_api.js') ?>"></script> <script src="<?= asset_url('assets/js/backend_calendar_api.js') ?>"></script>
<script> <script>
var GlobalVariables = { var GlobalVariables = {
@ -44,7 +44,7 @@
<div id="calendar-filter" class="col-12 col-sm-5"> <div id="calendar-filter" class="col-12 col-sm-5">
<div class="form-group calendar-filter-items"> <div class="form-group calendar-filter-items">
<select id="select-filter-item" class="form-control col" <select id="select-filter-item" class="form-control col"
title="<?= lang('select_filter_item_hint') ?>"> data-tippy-content="<?= lang('select_filter_item_hint') ?>">
</select> </select>
</div> </div>
</div> </div>
@ -53,13 +53,13 @@
<?php if (($role_slug == DB_SLUG_ADMIN || $role_slug == DB_SLUG_PROVIDER) <?php if (($role_slug == DB_SLUG_ADMIN || $role_slug == DB_SLUG_PROVIDER)
&& config('google_sync_feature') == TRUE): ?> && config('google_sync_feature') == TRUE): ?>
<button id="google-sync" class="btn btn-primary" <button id="google-sync" class="btn btn-primary"
title="<?= lang('trigger_google_sync_hint') ?>"> data-tippy-content="<?= lang('trigger_google_sync_hint') ?>">
<i class="fas fa-sync-alt"></i> <i class="fas fa-sync-alt"></i>
<span><?= lang('synchronize') ?></span> <span><?= lang('synchronize') ?></span>
</button> </button>
<button id="enable-sync" class="btn btn-light" data-toggle="button" <button id="enable-sync" class="btn btn-light" data-toggle="button"
title="<?= lang('enable_appointment_sync_hint') ?>"> data-tippy-content="<?= lang('enable_appointment_sync_hint') ?>">
<i class="far fa-calendar-alt mr-2"></i> <i class="far fa-calendar-alt mr-2"></i>
<span><?= lang('enable_sync') ?></span> <span><?= lang('enable_sync') ?></span>
</button> </button>
@ -90,20 +90,20 @@
</div> </div>
<?php endif ?> <?php endif ?>
<button id="reload-appointments" class="btn btn-light" title="<?= lang('reload_appointments_hint') ?>"> <button id="reload-appointments" class="btn btn-light" data-tippy-content="<?= lang('reload_appointments_hint') ?>">
<i class="fas fa-sync-alt"></i> <i class="fas fa-sync-alt"></i>
</button> </button>
<?php if($calendar_view === 'default'): ?> <?php if($calendar_view === 'default'): ?>
<a class="btn btn-light" href="<?= site_url('backend?view=table') ?>" <a class="btn btn-light" href="<?= site_url('backend?view=table') ?>"
title="<?= lang('table') ?>"> data-tippy-content="<?= lang('table') ?>">
<i class="fas fa-table"></i> <i class="fas fa-table"></i>
</a> </a>
<?php endif ?> <?php endif ?>
<?php if($calendar_view === 'table'): ?> <?php if($calendar_view === 'table'): ?>
<a class="btn btn-light" href="<?= site_url('backend?view=default') ?>" <a class="btn btn-light" href="<?= site_url('backend?view=default') ?>"
title="<?= lang('default') ?>"> data-tippy-content="<?= lang('default') ?>">
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt"></i>
</a> </a>
<?php endif ?> <?php endif ?>
@ -272,12 +272,12 @@
<legend> <legend>
<?= lang('customer_details_title') ?> <?= lang('customer_details_title') ?>
<button id="new-customer" class="btn btn-outline-secondary btn-sm" type="button" <button id="new-customer" class="btn btn-outline-secondary btn-sm" type="button"
title="<?= lang('clear_fields_add_existing_customer_hint') ?>"> data-tippy-content="<?= lang('clear_fields_add_existing_customer_hint') ?>">
<i class="far fa-plus-square mr-2"></i> <i class="far fa-plus-square mr-2"></i>
<?= lang('new') ?> <?= lang('new') ?>
</button> </button>
<button id="select-customer" class="btn btn-outline-secondary btn-sm" type="button" <button id="select-customer" class="btn btn-outline-secondary btn-sm" type="button"
title="<?= lang('pick_existing_customer_hint') ?>"> data-tippy-content="<?= lang('pick_existing_customer_hint') ?>">
<i class="far fa-hand-pointer mr-2"></i> <i class="far fa-hand-pointer mr-2"></i>
<span> <span>
<?= lang('select') ?> <?= lang('select') ?>

View file

@ -35,10 +35,10 @@
<div class="input-group-addon"> <div class="input-group-addon">
<div> <div>
<button class="filter btn btn-outline-secondary" type="submit" title="<?= lang('filter') ?>"> <button class="filter btn btn-outline-secondary" type="submit" data-tippy-content="<?= lang('filter') ?>">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
<button class="clear btn btn-outline-secondary" type="button" title="<?= lang('clear') ?>"> <button class="clear btn btn-outline-secondary" type="button" data-tippy-content="<?= lang('clear') ?>">
<i class="fas fa-redo-alt"></i> <i class="fas fa-redo-alt"></i>
</button> </button>
</div> </div>

View file

@ -16,6 +16,7 @@
<?= lang('licensed_under') ?> GPLv3 | <?= lang('licensed_under') ?> GPLv3 |
<span id="select-language" class="badge badge-secondary"> <span id="select-language" class="badge badge-secondary">
<i class="fas fa-language mr-2"></i>
<?= ucfirst(config('language')) ?> <?= ucfirst(config('language')) ?>
</span> </span>

View file

@ -8,7 +8,6 @@
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/bootstrap/css/bootstrap.min.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/bootstrap/css/bootstrap.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/jquery-ui/jquery-ui.min.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/jquery-ui/jquery-ui.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/jquery-qtip/jquery.qtip.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/trumbowyg/ui/trumbowyg.min.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/trumbowyg/ui/trumbowyg.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/select2/select2.min.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/ext/select2/select2.min.css') ?>">
<link rel="stylesheet" type="text/css" href="<?= asset_url('assets/css/backend.css') ?>"> <link rel="stylesheet" type="text/css" href="<?= asset_url('assets/css/backend.css') ?>">
@ -25,8 +24,9 @@
<script src="<?= asset_url('assets/ext/jquery/jquery.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/jquery/jquery.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/bootstrap/js/bootstrap.bundle.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/bootstrap/js/bootstrap.bundle.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/popper/popper.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/tippy/tippy-bundle.umd.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/jquery-ui/jquery-ui.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/jquery-ui/jquery-ui.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/jquery-qtip/jquery.qtip.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/moment/moment.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/moment/moment.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/datejs/date.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/datejs/date.min.js') ?>"></script>
<script src="<?= asset_url('assets/ext/trumbowyg/trumbowyg.min.js') ?>"></script> <script src="<?= asset_url('assets/ext/trumbowyg/trumbowyg.min.js') ?>"></script>
@ -38,7 +38,8 @@
<nav id="header" class="navbar navbar-expand-md navbar-dark"> <nav id="header" class="navbar navbar-expand-md navbar-dark">
<div id="header-logo" class="navbar-brand"> <div id="header-logo" class="navbar-brand">
<img src="<?= base_url('assets/img/logo.png') ?>"> <img src="<?= base_url('assets/img/logo.png') ?>">
<span>EASY!APPOINTMENTS</span> <h6>EASY!APPOINTMENTS</h6>
<small>Open Source Appointment Scheduler</small>
</div> </div>
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#header-menu"> <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#header-menu">
@ -52,7 +53,7 @@
<?php $active = ($active_menu == PRIV_APPOINTMENTS) ? 'active' : '' ?> <?php $active = ($active_menu == PRIV_APPOINTMENTS) ? 'active' : '' ?>
<li class="nav-item <?= $active . $hidden ?>"> <li class="nav-item <?= $active . $hidden ?>">
<a href="<?= site_url('backend') ?>" class="nav-link" <a href="<?= site_url('backend') ?>" class="nav-link"
title="<?= lang('manage_appointment_record_hint') ?>"> data-tippy-content="<?= lang('manage_appointment_record_hint') ?>">
<i class="far fa-calendar-alt mr-2"></i> <i class="far fa-calendar-alt mr-2"></i>
<?= lang('calendar') ?> <?= lang('calendar') ?>
</a> </a>
@ -62,7 +63,7 @@
<?php $active = ($active_menu == PRIV_CUSTOMERS) ? 'active' : '' ?> <?php $active = ($active_menu == PRIV_CUSTOMERS) ? 'active' : '' ?>
<li class="nav-item <?= $active . $hidden ?>"> <li class="nav-item <?= $active . $hidden ?>">
<a href="<?= site_url('backend/customers') ?>" class="nav-link" <a href="<?= site_url('backend/customers') ?>" class="nav-link"
title="<?= lang('manage_customers_hint') ?>"> data-tippy-content="<?= lang('manage_customers_hint') ?>">
<i class="fas fa-user-friends mr-2"></i> <i class="fas fa-user-friends mr-2"></i>
<?= lang('customers') ?> <?= lang('customers') ?>
</a> </a>
@ -72,7 +73,7 @@
<?php $active = ($active_menu == PRIV_SERVICES) ? 'active' : '' ?> <?php $active = ($active_menu == PRIV_SERVICES) ? 'active' : '' ?>
<li class="nav-item <?= $active . $hidden ?>"> <li class="nav-item <?= $active . $hidden ?>">
<a href="<?= site_url('backend/services') ?>" class="nav-link" <a href="<?= site_url('backend/services') ?>" class="nav-link"
title="<?= lang('manage_services_hint') ?>"> data-tippy-content="<?= lang('manage_services_hint') ?>">
<i class="fas fa-business-time mr-2"></i> <i class="fas fa-business-time mr-2"></i>
<?= lang('services') ?> <?= lang('services') ?>
</a> </a>
@ -82,7 +83,7 @@
<?php $active = ($active_menu == PRIV_USERS) ? 'active' : '' ?> <?php $active = ($active_menu == PRIV_USERS) ? 'active' : '' ?>
<li class="nav-item <?= $active . $hidden ?>"> <li class="nav-item <?= $active . $hidden ?>">
<a href="<?= site_url('backend/users') ?>" class="nav-link" <a href="<?= site_url('backend/users') ?>" class="nav-link"
title="<?= lang('manage_users_hint') ?>"> data-tippy-content="<?= lang('manage_users_hint') ?>">
<i class="fas fa-users-cog mr-2"></i> <i class="fas fa-users-cog mr-2"></i>
<?= lang('users') ?> <?= lang('users') ?>
</a> </a>
@ -93,7 +94,7 @@
<?php $active = ($active_menu == PRIV_SYSTEM_SETTINGS) ? 'active' : '' ?> <?php $active = ($active_menu == PRIV_SYSTEM_SETTINGS) ? 'active' : '' ?>
<li class="nav-item <?= $active . $hidden ?>"> <li class="nav-item <?= $active . $hidden ?>">
<a href="<?= site_url('backend/settings') ?>" class="nav-link" <a href="<?= site_url('backend/settings') ?>" class="nav-link"
title="<?= lang('settings_hint') ?>"> data-tippy-content="<?= lang('settings_hint') ?>">
<i class="fas fa-cogs mr-2"></i> <i class="fas fa-cogs mr-2"></i>
<?= lang('settings') ?> <?= lang('settings') ?>
</a> </a>
@ -101,7 +102,7 @@
<li class="nav-item"> <li class="nav-item">
<a href="<?= site_url('user/logout') ?>" class="nav-link" <a href="<?= site_url('user/logout') ?>" class="nav-link"
title="<?= lang('log_out_hint') ?>"> data-tippy-content="<?= lang('log_out_hint') ?>">
<i class="fas fa-sign-out-alt mr-2"></i> <i class="fas fa-sign-out-alt mr-2"></i>
<?= lang('log_out') ?> <?= lang('log_out') ?>
</a> </a>

View file

@ -51,10 +51,10 @@
<div class="input-group-addon"> <div class="input-group-addon">
<div> <div>
<button class="filter btn btn-outline-secondary" type="submit" title="<?= lang('filter') ?>"> <button class="filter btn btn-outline-secondary" type="submit" data-tippy-content="<?= lang('filter') ?>">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
<button class="clear btn btn-outline-secondary" type="button" title="<?= lang('clear') ?>"> <button class="clear btn btn-outline-secondary" type="button" data-tippy-content="<?= lang('clear') ?>">
<i class="fas fa-redo-alt"></i> <i class="fas fa-redo-alt"></i>
</button> </button>
</div> </div>
@ -192,10 +192,10 @@
<div class="input-group-addon"> <div class="input-group-addon">
<div> <div>
<button class="filter btn btn-outline-secondary" type="submit" title="<?= lang('filter') ?>"> <button class="filter btn btn-outline-secondary" type="submit" data-tippy-content="<?= lang('filter') ?>">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
<button class="clear btn btn-outline-secondary" type="button" title="<?= lang('clear') ?>"> <button class="clear btn btn-outline-secondary" type="button" data-tippy-content="<?= lang('clear') ?>">
<i class="fas fa-redo-alt"></i> <i class="fas fa-redo-alt"></i>
</button> </button>
</div> </div>

View file

@ -70,7 +70,7 @@
<?= lang('general_settings') ?> <?= lang('general_settings') ?>
<?php if ($privileges[PRIV_SYSTEM_SETTINGS]['edit'] == TRUE): ?> <?php if ($privileges[PRIV_SYSTEM_SETTINGS]['edit'] == TRUE): ?>
<button type="button" class="save-settings btn btn-primary btn-sm mb-2" <button type="button" class="save-settings btn btn-primary btn-sm mb-2"
title="<?= lang('save') ?>"> data-tippy-content="<?= lang('save') ?>">
<i class="far fa-check-square mr-2"></i> <i class="far fa-check-square mr-2"></i>
<?= lang('save') ?> <?= lang('save') ?>
</button> </button>
@ -223,7 +223,7 @@
<?= lang('business_logic') ?> <?= lang('business_logic') ?>
<?php if ($privileges[PRIV_SYSTEM_SETTINGS]['edit'] == TRUE): ?> <?php if ($privileges[PRIV_SYSTEM_SETTINGS]['edit'] == TRUE): ?>
<button type="button" class="save-settings btn btn-primary btn-sm mb-2" <button type="button" class="save-settings btn btn-primary btn-sm mb-2"
title="<?= lang('save') ?>"> data-tippy-content="<?= lang('save') ?>">
<i class="far fa-check-square mr-2"></i> <i class="far fa-check-square mr-2"></i>
<?= lang('save') ?> <?= lang('save') ?>
</button> </button>
@ -311,7 +311,7 @@
<?= lang('legal_contents') ?> <?= lang('legal_contents') ?>
<?php if ($privileges[PRIV_SYSTEM_SETTINGS]['edit'] == TRUE): ?> <?php if ($privileges[PRIV_SYSTEM_SETTINGS]['edit'] == TRUE): ?>
<button type="button" class="save-settings btn btn-primary btn-sm mb-2" <button type="button" class="save-settings btn btn-primary btn-sm mb-2"
title="<?= lang('save') ?>"> data-tippy-content="<?= lang('save') ?>">
<i class="far fa-check-square mr-2"></i> <i class="far fa-check-square mr-2"></i>
<?= lang('save') ?> <?= lang('save') ?>
</button> </button>
@ -392,7 +392,7 @@
<?= lang('personal_information') ?> <?= lang('personal_information') ?>
<?php if ($privileges[PRIV_USER_SETTINGS]['edit'] == TRUE): ?> <?php if ($privileges[PRIV_USER_SETTINGS]['edit'] == TRUE): ?>
<button type="button" class="save-settings btn btn-primary btn-sm mb-2" <button type="button" class="save-settings btn btn-primary btn-sm mb-2"
title="<?= lang('save') ?>"> data-tippy-content="<?= lang('save') ?>">
<i class="far fa-check-square mr-2"></i> <i class="far fa-check-square mr-2"></i>
<?= lang('save') ?> <?= lang('save') ?>
</button> </button>

View file

@ -68,10 +68,10 @@
<span class="input-group-addon"> <span class="input-group-addon">
<div> <div>
<button class="filter btn btn-outline-secondary" type="submit" title="<?= lang('filter') ?>"> <button class="filter btn btn-outline-secondary" type="submit" data-tippy-content="<?= lang('filter') ?>">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
<button class="clear btn btn-outline-secondary" type="button" title="<?= lang('clear') ?>"> <button class="clear btn btn-outline-secondary" type="button" data-tippy-content="<?= lang('clear') ?>">
<i class="fas fa-redo-alt"></i> <i class="fas fa-redo-alt"></i>
</button> </button>
</div> </div>
@ -284,7 +284,7 @@
<div class="working-plan-view tab-pane fade clearfix" id="working-plan"> <div class="working-plan-view tab-pane fade clearfix" id="working-plan">
<h3><?= lang('working_plan') ?></h3> <h3><?= lang('working_plan') ?></h3>
<button id="reset-working-plan" class="btn btn-primary" <button id="reset-working-plan" class="btn btn-primary"
title="<?= lang('reset_working_plan') ?>"> data-tippy-content="<?= lang('reset_working_plan') ?>">
<i class="fas fa-redo-alt mr-2"></i> <i class="fas fa-redo-alt mr-2"></i>
<?= lang('reset_plan') ?></button> <?= lang('reset_plan') ?></button>
<table class="working-plan table table-striped mt-2"> <table class="working-plan table table-striped mt-2">
@ -374,10 +374,10 @@
<span class="input-group-addon"> <span class="input-group-addon">
<div> <div>
<button class="filter btn btn-outline-secondary" type="submit" title="<?= lang('filter') ?>"> <button class="filter btn btn-outline-secondary" type="submit" data-tippy-content="<?= lang('filter') ?>">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
<button class="clear btn btn-outline-secondary" type="button" title="<?= lang('clear') ?>"> <button class="clear btn btn-outline-secondary" type="button" data-tippy-content="<?= lang('clear') ?>">
<i class="fas fa-redo-alt"></i> <i class="fas fa-redo-alt"></i>
</button> </button>
</div> </div>
@ -580,10 +580,10 @@
<span class="input-group-addon"> <span class="input-group-addon">
<div> <div>
<button class="filter btn btn-outline-secondary" type="submit" title="<?= lang('filter') ?>"> <button class="filter btn btn-outline-secondary" type="submit" data-tippy-content="<?= lang('filter') ?>">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
<button class="clear btn btn-outline-secondary" type="button" title="<?= lang('clear') ?>"> <button class="clear btn btn-outline-secondary" type="button" data-tippy-content="<?= lang('clear') ?>">
<i class="fas fa-redo-alt"></i> <i class="fas fa-redo-alt"></i>
</button> </button>
</div> </div>

View file

@ -17,10 +17,9 @@
</div> </div>
<header> <header>
<a href="https://easyappointments.org" target="_blank"> <div class="container">
<img src="<?= base_url('assets/img/installation-banner.png') ?>" <h1 class="page-title">Easy!Appointments Installation</h1>
alt="Easy!Appointments Installation Banner"> </div>
</a>
</header> </header>
<div class="content container-fluid"> <div class="content container-fluid">

View file

@ -29,6 +29,12 @@ root {
#header #header-logo { #header #header-logo {
padding: 5px; padding: 5px;
line-height: 0;
}
#header #header-logo small {
font-size: 9px;
color: #2e6a5b;
} }
#header #header-logo img { #header #header-logo img {
@ -38,12 +44,11 @@ root {
margin-right: 10px; margin-right: 10px;
} }
#header #header-logo span { #header #header-logo h6 {
float: left; margin-top: 6px;
font-size: 14px; font-size: 15px;
font-weight: bold; font-weight: bold;
color: white; color: white;
margin-top: 12px;
} }
#header .navbar-toggler { #header .navbar-toggler {

View file

@ -147,6 +147,7 @@ body {
text-align: center; text-align: center;
color: #0bb98d; color: #0bb98d;
transition: all .3s linear; transition: all .3s linear;
cursor: default;
} }
#book-appointment-wizard .active-step { #book-appointment-wizard .active-step {
@ -171,10 +172,6 @@ body {
border-top: 1px solid #ebeef1; border-top: 1px solid #ebeef1;
} }
#book-appointment-wizard #steps .custom-qtip {
border-width: 2px;
}
#book-appointment-wizard #available-hours { #book-appointment-wizard #available-hours {
overflow: auto; overflow: auto;
margin: 15px 0; margin: 15px 0;

View file

@ -15,17 +15,6 @@
} }
} }
body .custom-qtip {
border: none;
border-radius: 0;
padding: 10px;
box-shadow: 1px 1px 3px #767676;
background: #EFFDF6;
font-size: 12px;
line-height: 20px;
color: #258D53;
}
body .ui-widget { body .ui-widget {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1.1em; font-size: 1.1em;

View file

@ -3,11 +3,21 @@ header {
margin-bottom: 25px; margin-bottom: 25px;
} }
header .page-title {
font-weight: lighter;
padding: 40px 0;
color: #429a82;
}
.content { .content {
margin: 32px; margin: 32px;
max-width: 980px; max-width: 980px;
} }
.content p {
word-break: break-all;
}
.alert { .alert {
margin: 25px 0 10px 0; margin: 25px 0 10px 0;
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
assets/ext/popper/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -25,7 +25,7 @@ window.Backend = window.Backend || {};
/** /**
* Main javascript code for the backend of Easy!Appointments. * Main javascript code for the backend of Easy!Appointments.
*/ */
$(document).ready(function () { $(function () {
$(window) $(window)
.on('resize', function () { .on('resize', function () {
Backend.placeFooterToBottom(); Backend.placeFooterToBottom();
@ -40,15 +40,7 @@ window.Backend = window.Backend || {};
$('#loading').hide(); $('#loading').hide();
}); });
$('.menu-item').qtip({ tippy('[data-tippy-content]');
position: {
my: 'top center',
at: 'bottom center'
},
style: {
classes: 'qtip-green qtip-shadow custom-qtip'
}
});
GeneralFunctions.enableLanguageSelection($('#select-language')); GeneralFunctions.enableLanguageSelection($('#select-language'));
}); });

View file

@ -114,7 +114,7 @@ window.BackendCalendar = window.BackendCalendar || {};
exports.initialize = function (view) { exports.initialize = function (view) {
BackendCalendarGoogleSync.initialize(); BackendCalendarGoogleSync.initialize();
BackendCalendarAppointmentsModal.initialize(); BackendCalendarAppointmentsModal.initialize();
BackendCalendarUnavailabilitiesModal.initialize(); BackendCalendarUnavailabilityEventsModal.initialize();
// Load and initialize the calendar view. // Load and initialize the calendar view.
if (view === 'table') { if (view === 'table') {

View file

@ -142,7 +142,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
endDatetime = Date.parseExact(unavailable.end_datetime, 'yyyy-MM-dd HH:mm:ss'); endDatetime = Date.parseExact(unavailable.end_datetime, 'yyyy-MM-dd HH:mm:ss');
$dialog = $('#manage-unavailable'); $dialog = $('#manage-unavailable');
BackendCalendarUnavailabilitiesModal.resetUnavailableDialog(); BackendCalendarUnavailabilityEventsModal.resetUnavailableDialog();
// Apply unavailable data to dialog. // Apply unavailable data to dialog.
$dialog.find('.modal-header h3').text('Edit Unavailable Period'); $dialog.find('.modal-header h3').text('Edit Unavailable Period');
@ -1066,7 +1066,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
// Add custom unavailable periods (they are always displayed on the calendar, even if the provider won't // Add custom unavailable periods (they are always displayed on the calendar, even if the provider won't
// work on that day). // work on that day).
var unavailableEvents = []; var unavailabilityEvents = [];
response.unavailables.forEach(function (unavailable) { response.unavailables.forEach(function (unavailable) {
var notes = unavailable.notes ? ' - ' + unavailable.notes : ''; var notes = unavailable.notes ? ' - ' + unavailable.notes : '';
@ -1074,7 +1074,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
notes = unavailable.notes.substring(0, 30) + '...' notes = unavailable.notes.substring(0, 30) + '...'
} }
var unavailableEvent = { var unavailabilityEvent = {
title: EALang.unavailable + notes, title: EALang.unavailable + notes,
start: moment(unavailable.start_datetime), start: moment(unavailable.start_datetime),
end: moment(unavailable.end_datetime), end: moment(unavailable.end_datetime),
@ -1085,10 +1085,10 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
data: unavailable data: unavailable
}; };
unavailableEvents.push(unavailableEvent); unavailabilityEvents.push(unavailabilityEvent);
}); });
$calendar.fullCalendar('addEventSource', unavailableEvents); $calendar.fullCalendar('addEventSource', unavailabilityEvents);
var calendarView = $('#calendar').fullCalendar('getView'); var calendarView = $('#calendar').fullCalendar('getView');
@ -1103,7 +1103,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
var workingPlan = jQuery.parseJSON(provider.settings.working_plan); var workingPlan = jQuery.parseJSON(provider.settings.working_plan);
var workingPlanExceptions = jQuery.parseJSON(provider.settings.working_plan_exceptions); var workingPlanExceptions = jQuery.parseJSON(provider.settings.working_plan_exceptions);
var unavailableEvent; var unavailabilityEvent;
var viewStart; var viewStart;
var viewEnd; var viewEnd;
var breakStart; var breakStart;
@ -1157,7 +1157,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
// Non-working day. // Non-working day.
if (sortedWorkingPlan[weekdayName] === null) { if (sortedWorkingPlan[weekdayName] === null) {
// Working plan exception. // Working plan exception.
unavailableEvent = { unavailabilityEvent = {
title: EALang.not_working, title: EALang.not_working,
start: calendarView.intervalStart.clone(), start: calendarView.intervalStart.clone(),
end: calendarView.intervalEnd.clone(), end: calendarView.intervalEnd.clone(),
@ -1167,7 +1167,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
className: 'fc-unavailable' className: 'fc-unavailable'
}; };
$calendar.fullCalendar('renderEvent', unavailableEvent, false); $calendar.fullCalendar('renderEvent', unavailabilityEvent, false);
return; // Go to next loop. return; // Go to next loop.
} }
@ -1276,7 +1276,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
// Non-working day. // Non-working day.
if (sortedWorkingPlan[weekdayName] === null) { if (sortedWorkingPlan[weekdayName] === null) {
// Add a full day unavailable event. // Add a full day unavailable event.
unavailableEvent = { unavailabilityEvent = {
title: EALang.not_working, title: EALang.not_working,
start: calendarDate.clone(), start: calendarDate.clone(),
end: calendarDate.clone().add(1, 'day'), end: calendarDate.clone().add(1, 'day'),
@ -1286,7 +1286,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
className: 'fc-unavailable' className: 'fc-unavailable'
}; };
$calendar.fullCalendar('renderEvent', unavailableEvent, true); $calendar.fullCalendar('renderEvent', unavailabilityEvent, true);
calendarDate.add(1, 'day'); calendarDate.add(1, 'day');
@ -1300,7 +1300,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
workDateStart.minute(parseInt(startHour[1])); workDateStart.minute(parseInt(startHour[1]));
if (calendarDate < workDateStart) { if (calendarDate < workDateStart) {
unavailableEvent = { unavailabilityEvent = {
title: EALang.not_working, title: EALang.not_working,
start: calendarDate.clone(), start: calendarDate.clone(),
end: moment(calendarDate.format('YYYY-MM-DD') + ' ' + sortedWorkingPlan[weekdayName].start + ':00'), end: moment(calendarDate.format('YYYY-MM-DD') + ' ' + sortedWorkingPlan[weekdayName].start + ':00'),
@ -1310,7 +1310,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
className: 'fc-unavailable' className: 'fc-unavailable'
}; };
$calendar.fullCalendar('renderEvent', unavailableEvent, true); $calendar.fullCalendar('renderEvent', unavailabilityEvent, true);
} }
// Add unavailable period after work ends. // Add unavailable period after work ends.
@ -1320,7 +1320,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
workDateEnd.minute(parseInt(endHour[1])); workDateEnd.minute(parseInt(endHour[1]));
if (calendarView.end > workDateEnd) { if (calendarView.end > workDateEnd) {
unavailableEvent = { unavailabilityEvent = {
title: EALang.not_working, title: EALang.not_working,
start: moment(calendarDate.format('YYYY-MM-DD') + ' ' + sortedWorkingPlan[weekdayName].end + ':00'), start: moment(calendarDate.format('YYYY-MM-DD') + ' ' + sortedWorkingPlan[weekdayName].end + ':00'),
end: calendarDate.clone().add(1, 'day'), end: calendarDate.clone().add(1, 'day'),
@ -1330,7 +1330,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
className: 'fc-unavailable' className: 'fc-unavailable'
}; };
$calendar.fullCalendar('renderEvent', unavailableEvent, false); $calendar.fullCalendar('renderEvent', unavailabilityEvent, false);
} }
// Add unavailable periods during day breaks. // Add unavailable periods during day breaks.
@ -1345,7 +1345,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
breakEnd.hour(parseInt(breakEndString[0])); breakEnd.hour(parseInt(breakEndString[0]));
breakEnd.minute(parseInt(breakEndString[1])); breakEnd.minute(parseInt(breakEndString[1]));
var unavailableEvent = { var unavailabilityEvent = {
title: EALang.break, title: EALang.break,
start: moment(calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.start), start: moment(calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.start),
end: moment(calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.end), end: moment(calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.end),
@ -1355,7 +1355,7 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
className: 'fc-unavailable fc-break' className: 'fc-unavailable fc-break'
}; };
$calendar.fullCalendar('renderEvent', unavailableEvent, false); $calendar.fullCalendar('renderEvent', unavailabilityEvent, false);
}); });
calendarDate.add(1, 'day'); calendarDate.add(1, 'day');
@ -1612,27 +1612,6 @@ window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {};
$dialog.modal('show'); $dialog.modal('show');
} }
// Apply qtip to control tooltips.
$('#calendar-toolbar button').qtip({
position: {
my: 'top center',
at: 'bottom center'
},
style: {
classes: 'qtip-green qtip-shadow custom-qtip'
}
});
$('#select-filter-item').qtip({
position: {
my: 'middle left',
at: 'middle right'
},
style: {
classes: 'qtip-green qtip-shadow custom-qtip'
}
});
if (!$('#select-filter-item option').length) { if (!$('#select-filter-item option').length) {
$('#calendar-actions button').prop('disabled', true); $('#calendar-actions button').prop('disabled', true);
} }

View file

@ -91,13 +91,16 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
$providerColumn.find('.calendar-wrapper').fullCalendar('removeEvents'); $providerColumn.find('.calendar-wrapper').fullCalendar('removeEvents');
createNonWorkingHours($providerColumn.find('.calendar-wrapper'), JSON.parse($providerColumn.data('provider').settings.working_plan)); createNonWorkingHours(
$providerColumn.find('.calendar-wrapper'),
$providerColumn.data('provider')
);
// Add the appointments to the column. // Add the appointments to the column.
createAppointments($providerColumn, response.appointments); createAppointments($providerColumn, response.appointments);
// Add the unavailabilities to the column. // Add the unavailability events to the column.
createUnavailabilities($providerColumn, response.unavailabilities); createUnavailabilityEvents($providerColumn, response.unavailability_events);
// Add the provider breaks to the column. // Add the provider breaks to the column.
var workingPlan = JSON.parse(provider.settings.working_plan); var workingPlan = JSON.parse(provider.settings.working_plan);
@ -145,7 +148,36 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
var $dialog; var $dialog;
if (lastFocusedEventData.data.is_unavailable === '0') { if (lastFocusedEventData.data.workingPlanException) {
var date = lastFocusedEventData.data.date;
var workingPlanException = lastFocusedEventData.data.workingPlanException;
var provider = lastFocusedEventData.data.provider;
WorkingPlanExceptionsModal
.edit(date, workingPlanException)
.done(function (date, workingPlanException) {
var successCallback = function () {
Backend.displayNotification(EALang.working_plan_exception_saved);
var workingPlanExceptions = jQuery.parseJSON(provider.settings.working_plan_exceptions) || {};
workingPlanExceptions[date] = workingPlanException;
for (var index in GlobalVariables.availableProviders) {
var availableProvider = GlobalVariables.availableProviders[index];
if (Number(availableProvider.id) === Number(provider.id)) {
availableProvider.settings.working_plan_exceptions = JSON.stringify(workingPlanExceptions);
break;
}
}
$('#select-filter-item').trigger('change'); // Update the calendar.
};
BackendCalendarApi.saveWorkingPlanException(date, workingPlanException, provider.id, successCallback, null);
});
} else if (lastFocusedEventData.data.is_unavailable === '0') {
var appointment = lastFocusedEventData.data; var appointment = lastFocusedEventData.data;
$dialog = $('#manage-appointment'); $dialog = $('#manage-appointment');
@ -186,7 +218,7 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
var endDatetime = Date.parseExact(unavailable.end_datetime, 'yyyy-MM-dd HH:mm:ss'); var endDatetime = Date.parseExact(unavailable.end_datetime, 'yyyy-MM-dd HH:mm:ss');
$dialog = $('#manage-unavailable'); $dialog = $('#manage-unavailable');
BackendCalendarUnavailabilitiesModal.resetUnavailableDialog(); BackendCalendarUnavailabilityEventsModal.resetUnavailableDialog();
// Apply unavailable data to dialog. // Apply unavailable data to dialog.
$dialog.find('.modal-header h3').text('Edit Unavailable Period'); $dialog.find('.modal-header h3').text('Edit Unavailable Period');
@ -592,13 +624,16 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
createCalendar($providerColumn, date, provider); createCalendar($providerColumn, date, provider);
// Create non working hours. // Create non working hours.
createNonWorkingHours($providerColumn.find('.calendar-wrapper'), JSON.parse(provider.settings.working_plan)) createNonWorkingHours(
$providerColumn.find('.calendar-wrapper'),
provider
);
// Add the appointments to the column. // Add the appointments to the column.
createAppointments($providerColumn, events.appointments); createAppointments($providerColumn, events.appointments);
// Add the unavailabilities to the column. // Add the unavailability events to the column.
createUnavailabilities($providerColumn, events.unavailabilities); createUnavailabilityEvents($providerColumn, events.unavailability_events);
Backend.placeFooterToBottom(); Backend.placeFooterToBottom();
} }
@ -606,8 +641,8 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
/** /**
* Get Calendar Component Height * Get Calendar Component Height
* *
* This method calculates the proper calendar height, in order to be displayed correctly, even when the * This method calculates the proper calendar height, in order to be displayed correctly, even when the browser
* browser window is resizing. * window is resizing.
* *
* @return {Number} Returns the calendar element height in pixels. * @return {Number} Returns the calendar element height in pixels.
*/ */
@ -793,11 +828,38 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
$(element).fullCalendar('option', 'height', getCalendarHeight()); $(element).fullCalendar('option', 'height', getCalendarHeight());
} }
function createNonWorkingHours($calendar, workingPlan) { function createNonWorkingHours($calendar, provider) {
var workingPlan = JSON.parse(provider.settings.working_plan);
var workingPlanExceptions = JSON.parse(provider.settings.working_plan_exceptions);
var view = $calendar.fullCalendar('getView'); var view = $calendar.fullCalendar('getView');
var start = view.start.clone(); var start = view.start.clone();
var end = view.end.clone(); var end = view.end.clone();
var selDayName = start.toDate().toString('dddd').toLowerCase(); var selDayName = start.toDate().toString('dddd').toLowerCase();
var selDayDate = start.format('YYYY-MM-DD');
if (workingPlanExceptions[selDayDate]) {
workingPlan[selDayName] = workingPlanExceptions[selDayDate];
var workingPlanExceptionStart = selDayDate + ' ' + workingPlan[selDayName].start;
var workingPlanExceptionEnd = selDayDate + ' ' + workingPlan[selDayName].end;
var workingPlanExceptionEvent = {
title: EALang.working_plan_exception,
start: moment(workingPlanExceptionStart, 'YYYY-MM-DD HH:mm', true),
end: moment(workingPlanExceptionEnd, 'YYYY-MM-DD HH:mm', true).add(1, 'day'),
allDay: true,
color: '#879DB4',
editable: false,
className: 'fc-working-plan-exception fc-custom',
data: {
date: selDayDate,
workingPlanException: workingPlanExceptions[selDayDate],
provider: provider
}
};
$calendar.fullCalendar('renderEvent', workingPlanExceptionEvent, false);
}
if (workingPlan[selDayName] === null) { if (workingPlan[selDayName] === null) {
var nonWorkingDay = { var nonWorkingDay = {
@ -912,20 +974,20 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
} }
/** /**
* Create Unavailabilities Events * Create Unavailability Events
* *
* This method will add the unavailability events on the table view. * This method will add the unavailability events on the table view.
* *
* @param {jQuery} $providerColumn The provider column container. * @param {jQuery} $providerColumn The provider column container.
* @param {Object[]} unavailabilities Contains the unavailability events data. * @param {Object[]} unavailabilityEvents Contains the unavailability events data.
*/ */
function createUnavailabilities($providerColumn, unavailabilities) { function createUnavailabilityEvents($providerColumn, unavailabilityEvents) {
if (unavailabilities.length === 0) { if (unavailabilityEvents.length === 0) {
return; return;
} }
for (var index in unavailabilities) { for (var index in unavailabilityEvents) {
var unavailability = unavailabilities[index]; var unavailability = unavailabilityEvents[index];
if (unavailability.id_users_provider !== $providerColumn.data('provider').id) { if (unavailability.id_users_provider !== $providerColumn.data('provider').id) {
continue; continue;
@ -1036,10 +1098,10 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
if ($(this).hasClass('fc-unavailable') || $parent.hasClass('fc-unavailable') || $altParent.hasClass('fc-unavailable')) { if ($(this).hasClass('fc-unavailable') || $parent.hasClass('fc-unavailable') || $altParent.hasClass('fc-unavailable')) {
displayEdit = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) displayEdit = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom'))
&& GlobalVariables.user.privileges.appointments.edit === true) && GlobalVariables.user.privileges.appointments.edit === true)
? 'mr-2' : 'd-none'; ? '' : 'd-none';
displayDelete = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) displayDelete = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom'))
&& GlobalVariables.user.privileges.appointments.delete === true) && GlobalVariables.user.privileges.appointments.delete === true)
? 'mr-2' : 'd-none'; // Same value at the time. ? '' : 'd-none'; // Same value at the time.
$html = $('<div/>', { $html = $('<div/>', {
'html': [ 'html': [
@ -1070,8 +1132,30 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
$('<hr/>'), $('<hr/>'),
$('<div/>', { $('<div/>', {
'class': 'd-flex justify-content-between', 'class': 'd-flex justify-content-center',
'html': [ 'html': [
$('<button/>', {
'class': 'close-popover btn btn-outline-secondary mr-2',
'html': [
$('<i/>', {
'class': 'fas fa-ban mr-2'
}),
$('<span/>', {
'text': EALang.close
})
]
}),
$('<button/>', {
'class': 'delete-popover btn btn-outline-secondary mr-2 ' + displayDelete,
'html': [
$('<i/>', {
'class': 'far fa-trash-alt mr-2'
}),
$('<span/>', {
'text': EALang.delete
})
]
}),
$('<button/>', { $('<button/>', {
'class': 'edit-popover btn btn-primary ' + displayEdit, 'class': 'edit-popover btn btn-primary ' + displayEdit,
'html': [ 'html': [
@ -1083,28 +1167,6 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
}) })
] ]
}), }),
$('<button/>', {
'class': 'delete-popover btn btn-outline-secondary ' + displayDelete,
'html': [
$('<i/>', {
'class': 'far fa-trash-alt mr-2'
}),
$('<span/>', {
'text': EALang.delete
})
]
}),
$('<button/>', {
'class': 'close-popover btn btn-outline-secondary',
'html': [
$('<i/>', {
'class': 'fas fa-ban mr-2'
}),
$('<span/>', {
'text': EALang.close
})
]
})
] ]
}) })
] ]
@ -1112,11 +1174,11 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
} else if ($(this).hasClass('fc-working-plan-exception') || $parent.hasClass('fc-working-plan-exception') || $altParent.hasClass('fc-working-plan-exception')) { } else if ($(this).hasClass('fc-working-plan-exception') || $parent.hasClass('fc-working-plan-exception') || $altParent.hasClass('fc-working-plan-exception')) {
displayEdit = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) displayEdit = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom'))
&& GlobalVariables.user.privileges.appointments.edit === true) && GlobalVariables.user.privileges.appointments.edit === true)
? 'mr-2' : 'd-none'; // Same value at the time. ? '' : 'd-none'; // Same value at the time.
displayDelete = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) displayDelete = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom'))
&& GlobalVariables.user.privileges.appointments.delete === true) && GlobalVariables.user.privileges.appointments.delete === true)
? 'mr-2' : 'd-none'; // Same value at the time. ? '' : 'd-none'; // Same value at the time.
$html = $('<div/>', { $html = $('<div/>', {
'html': [ 'html': [
@ -1124,7 +1186,7 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
'text': EALang.provider 'text': EALang.provider
}), }),
$('<span/>', { $('<span/>', {
'text': event.data ? event.data.first_name + ' ' + event.data.last_name : '-' 'text': event.data ? event.data.provider.first_name + ' ' + event.data.provider.last_name : '-'
}), }),
$('<br/>'), $('<br/>'),
@ -1158,18 +1220,18 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
'class': 'd-flex justify-content-center', 'class': 'd-flex justify-content-center',
'html': [ 'html': [
$('<button/>', { $('<button/>', {
'class': 'edit-popover btn btn-danger ' + displayEdit, 'class': 'close-popover btn btn-outline-secondary mr-2',
'html': [ 'html': [
$('<i/>', { $('<i/>', {
'class': 'far fa-edit mr-2' 'class': 'fas fa-ban mr-2'
}), }),
$('<span/>', { $('<span/>', {
'text': EALang.edit 'text': EALang.close
}) })
] ]
}), }),
$('<button/>', { $('<button/>', {
'class': 'delete-popover btn btn-outline-secondary ' + displayDelete, 'class': 'delete-popover btn btn-outline-secondary mr-2 ' + displayDelete,
'html': [ 'html': [
$('<i/>', { $('<i/>', {
'class': 'far fa-trash-alt mr-2' 'class': 'far fa-trash-alt mr-2'
@ -1180,13 +1242,13 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
] ]
}), }),
$('<button/>', { $('<button/>', {
'class': 'close-popover btn btn-outline-secondary', 'class': 'edit-popover btn btn-primary ' + displayEdit,
'html': [ 'html': [
$('<i/>', { $('<i/>', {
'class': 'fas fa-ban mr-2' 'class': 'far fa-edit mr-2'
}), }),
$('<span/>', { $('<span/>', {
'text': EALang.close 'text': EALang.edit
}) })
] ]
}) })
@ -1196,9 +1258,9 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
}); });
} else { } else {
displayEdit = (GlobalVariables.user.privileges.appointments.edit === true) displayEdit = (GlobalVariables.user.privileges.appointments.edit === true)
? 'mr-2' : 'd-none'; ? '' : 'd-none';
displayDelete = (GlobalVariables.user.privileges.appointments.delete === true) displayDelete = (GlobalVariables.user.privileges.appointments.delete === true)
? 'mr-2' : 'd-none'; ? '' : 'd-none';
$html = $('<div/>', { $html = $('<div/>', {
'html': [ 'html': [
@ -1284,18 +1346,18 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
'class': 'd-flex justify-content-center', 'class': 'd-flex justify-content-center',
'html': [ 'html': [
$('<button/>', { $('<button/>', {
'class': 'edit-popover btn btn-primary ' + displayEdit, 'class': 'close-popover btn btn-outline-secondary mr-2',
'html': [ 'html': [
$('<i/>', { $('<i/>', {
'class': 'far fa-edit mr-2' 'class': 'fas fa-ban mr-2'
}), }),
$('<span/>', { $('<span/>', {
'text': EALang.edit 'text': EALang.close
}) })
] ]
}), }),
$('<button/>', { $('<button/>', {
'class': 'delete-popover btn btn-outline-secondary ' + displayDelete, 'class': 'delete-popover btn btn-outline-secondary mr-2' + displayDelete,
'html': [ 'html': [
$('<i/>', { $('<i/>', {
'class': 'far fa-trash-alt mr-2' 'class': 'far fa-trash-alt mr-2'
@ -1306,13 +1368,13 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
] ]
}), }),
$('<button/>', { $('<button/>', {
'class': 'close-popover btn btn-outline-secondary', 'class': 'edit-popover btn btn-primary ' + displayEdit,
'html': [ 'html': [
$('<i/>', { $('<i/>', {
'class': 'fas fa-ban mr-2' 'class': 'far fa-edit mr-2'
}), }),
$('<span/>', { $('<span/>', {
'text': EALang.close 'text': EALang.edit
}) })
] ]
}) })
@ -1679,6 +1741,8 @@ window.BackendCalendarTableView = window.BackendCalendarTableView || {};
createView(startDate, endDate); createView(startDate, endDate);
$('#insert-working-plan-exception').hide();
bindEventHandlers(); bindEventHandlers();
// Hide Google Calendar Sync buttons cause they can not be used within this view. // Hide Google Calendar Sync buttons cause they can not be used within this view.

View file

@ -10,13 +10,13 @@
* ---------------------------------------------------------------------------- */ * ---------------------------------------------------------------------------- */
/** /**
* Backend Calendar Unavailabilities Modal * Backend Calendar Unavailability Events Modal
* *
* This module implements the unavailabilities modal functionality. * This module implements the unavailability events modal functionality.
* *
* @module BackendCalendarUnavailabilitiesModal * @module BackendCalendarUnavailabilityEventsModal
*/ */
window.BackendCalendarUnavailabilitiesModal = window.BackendCalendarUnavailabilitiesModal || {}; window.BackendCalendarUnavailabilityEventsModal = window.BackendCalendarUnavailabilityEventsModal || {};
(function (exports) { (function (exports) {
@ -91,7 +91,7 @@ window.BackendCalendarUnavailabilitiesModal = window.BackendCalendarUnavailabili
* he cannot accept any appointments. * he cannot accept any appointments.
*/ */
$('#insert-unavailable').on('click', function () { $('#insert-unavailable').on('click', function () {
BackendCalendarUnavailabilitiesModal.resetUnavailableDialog(); BackendCalendarUnavailabilityEventsModal.resetUnavailableDialog();
var $dialog = $('#manage-unavailable'); var $dialog = $('#manage-unavailable');
// Set the default datetime values. // Set the default datetime values.
@ -229,4 +229,4 @@ window.BackendCalendarUnavailabilitiesModal = window.BackendCalendarUnavailabili
bindEventHandlers(); bindEventHandlers();
}; };
})(window.BackendCalendarUnavailabilitiesModal); })(window.BackendCalendarUnavailabilityEventsModal);

View file

@ -113,16 +113,6 @@ window.BackendUsers = window.BackendUsers || {};
.appendTo('#secretary-providers'); .appendTo('#secretary-providers');
}); });
$('#reset-working-plan').qtip({
position: {
my: 'top center',
at: 'bottom center'
},
style: {
classes: 'qtip-green qtip-shadow custom-qtip'
}
});
// Bind event handlers. // Bind event handlers.
if (defaultEventHandlers) { if (defaultEventHandlers) {
bindEventHandlers(); bindEventHandlers();

View file

@ -89,15 +89,7 @@ window.FrontendBook = window.FrontendBook || {};
FrontendBook.manageMode = manageMode; FrontendBook.manageMode = manageMode;
// Initialize page's components (tooltips, datepickers etc). // Initialize page's components (tooltips, datepickers etc).
$('.book-step').qtip({ tippy('[data-tippy-content]');
position: {
my: 'top center',
at: 'bottom center'
},
style: {
classes: 'qtip-green qtip-shadow custom-qtip'
}
});
var weekDayId = GeneralFunctions.getWeekDayId(GlobalVariables.firstWeekday); var weekDayId = GeneralFunctions.getWeekDayId(GlobalVariables.firstWeekday);
@ -632,7 +624,7 @@ window.FrontendBook = window.FrontendBook || {};
data.appointment = { data.appointment = {
start_datetime: $('#select-date').datepicker('getDate').toString('yyyy-MM-dd') start_datetime: $('#select-date').datepicker('getDate').toString('yyyy-MM-dd')
+ ' ' + Date.parse($('.selected-hour').text()).toString('HH:mm') + ':00', + ' ' + Date.parse($('.selected-hour').data('value') || '').toString('HH:mm') + ':00',
end_datetime: calculateEndDatetime(), end_datetime: calculateEndDatetime(),
notes: $('#notes').val(), notes: $('#notes').val(),
is_unavailable: false, is_unavailable: false,
@ -666,7 +658,7 @@ window.FrontendBook = window.FrontendBook || {};
// Add the duration to the start datetime. // Add the duration to the start datetime.
var startDatetime = $('#select-date').datepicker('getDate').toString('dd-MM-yyyy') var startDatetime = $('#select-date').datepicker('getDate').toString('dd-MM-yyyy')
+ ' ' + Date.parse($('.selected-hour').text()).toString('HH:mm'); + ' ' + Date.parse($('.selected-hour').data('value') || '').toString('HH:mm');
startDatetime = Date.parseExact(startDatetime, 'dd-MM-yyyy HH:mm'); startDatetime = Date.parseExact(startDatetime, 'dd-MM-yyyy HH:mm');
var endDatetime; var endDatetime;

View file

@ -90,7 +90,7 @@ window.FrontendBookApi = window.FrontendBookApi || {};
var selectedTimezone = $('#select-timezone').val(); var selectedTimezone = $('#select-timezone').val();
var timeFormat = GlobalVariables.timeFormat === 'regular' ? 'h:mm a' : 'HH:mm'; var timeFormat = GlobalVariables.timeFormat === 'regular' ? 'h:mm a' : 'HH:mm';
response.forEach(function (availableHour, index) { response.forEach(function (availableHour) {
var availableHourMoment = moment var availableHourMoment = moment
.tz(selectedDate + ' ' + availableHour + ':00', providerTimezone) .tz(selectedDate + ' ' + availableHour + ':00', providerTimezone)
.tz(selectedTimezone); .tz(selectedTimezone);
@ -98,6 +98,9 @@ window.FrontendBookApi = window.FrontendBookApi || {};
$('#available-hours').append( $('#available-hours').append(
$('<button/>', { $('<button/>', {
'class': 'btn btn-outline-secondary btn-block shadow-none available-hour', 'class': 'btn btn-outline-secondary btn-block shadow-none available-hour',
'data': {
'value': availableHour
},
'text': availableHourMoment.format(timeFormat) 'text': availableHourMoment.format(timeFormat)
}) })
); );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

9510
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -24,16 +24,17 @@
"node": ">=12" "node": ">=12"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.15.1",
"bootstrap": "^4.5.0", "bootstrap": "^4.5.3",
"cookieconsent": "^3.1.1", "cookieconsent": "^3.1.1",
"datejs": "0.0.2", "datejs": "0.0.2",
"fullcalendar": "^3.10.1", "fullcalendar": "^3.10.2",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"jquery-ui": "^1.12.0", "jquery-ui": "^1.12.0",
"moment": "^2.25.3", "moment": "^2.29.1",
"moment-timezone": "^0.5.28", "moment-timezone": "^0.5.31",
"qtip2": "^3.0.3", "qtip2": "^3.0.3",
"tippy.js": "^6.2.7",
"trumbowyg": "^2.21.0" "trumbowyg": "^2.21.0"
}, },
"devDependencies": { "devDependencies": {
@ -47,7 +48,7 @@
"gulp-plumber": "^1.2.1", "gulp-plumber": "^1.2.1",
"gulp-rename": "^1.4.0", "gulp-rename": "^1.4.0",
"gulp-uglify": "^3.0.2", "gulp-uglify": "^3.0.2",
"jsdoc": "^3.6.4", "jsdoc": "^3.6.6",
"node-notifier": "^5.4.3", "node-notifier": "^5.4.3",
"plato": "^1.7.0", "plato": "^1.7.0",
"zip-dir": "^1.0.2" "zip-dir": "^1.0.2"