Skip to content

Instantly share code, notes, and snippets.

Created April 13, 2015 14:16
Show Gist options
  • Save anonymous/9e49e4a67871f213a084 to your computer and use it in GitHub Desktop.
Save anonymous/9e49e4a67871f213a084 to your computer and use it in GitHub Desktop.
Change display of calendar to make only dates clickable in which there are free appointment slots
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Appointments extends CI_Controller {
public function __construct() {
parent::__construct();
$this->load->library('session');
// Set user's selected language.
if ($this->session->userdata('language')) {
$this->config->set_item('language', $this->session->userdata('language'));
$this->lang->load('translations', $this->session->userdata('language'));
} else {
$this->lang->load('translations', $this->config->item('language')); // default
}
}
/**
* Default callback method of the application.
*
* This method creates the appointment book wizard. If an appointment hash
* is provided then it means that the customer followed the appointment
* manage link that was send with the book success email.
*
* @param string $appointment_hash The db appointment hash of an existing
* record.
*/
public function index($appointment_hash = '') {
if (!$this->check_installation()) return;
$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('services_model');
$this->load->model('customers_model');
$this->load->model('settings_model');
if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') {
try {
$available_services = $this->services_model->get_available_services();
$available_providers = $this->providers_model->get_available_providers();
$company_name = $this->settings_model->get_setting('company_name');
// If an appointment hash is provided then it means that the customer
// is trying to edit a registered appointment record.
if ($appointment_hash !== ''){
// Load the appointments data and enable the manage mode of the page.
$manage_mode = TRUE;
$results = $this->appointments_model->get_batch(array('hash' => $appointment_hash));
if (count($results) === 0) {
// The requested appointment doesn't exist in the database. Display
// a message to the customer.
$view = array(
'message_title' => $this->lang->line('appointment_not_found'),
'message_text' => $this->lang->line('appointment_does_not_exist_in_db'),
'message_icon' => $this->config->item('base_url')
. 'assets/images/error.png',
'company_name' => $company_name
);
$this->load->view('appointments/message', $view);
return;
}
$appointment = $results[0];
$provider = $this->providers_model->get_row($appointment['id_users_provider']);
$customer = $this->customers_model->get_row($appointment['id_users_customer']);
} else {
// The customer is going to book a new appointment so there is no
// need for the manage functionality to be initialized.
$manage_mode = FALSE;
$appointment = array();
$provider = array();
$customer = array();
}
// Load the book appointment view.
$view = array (
'available_services' => $available_services,
'available_providers' => $available_providers,
'company_name' => $company_name,
'manage_mode' => $manage_mode,
'appointment_data' => $appointment,
'provider_data' => $provider,
'customer_data' => $customer
);
} catch(Exception $exc) {
$view['exceptions'][] = $exc;
}
$this->load->view('appointments/book', $view);
} else {
// The page is a post-back. Register the appointment and send notification emails
// to the provider and the customer that are related to the appointment. If google
// sync is enabled then add the appointment to the provider's account.
try {
$post_data = json_decode($_POST['post_data'], true);
$appointment = $post_data['appointment'];
$customer = $post_data['customer'];
if ($this->customers_model->exists($customer))
$customer['id'] = $this->customers_model->find_record_id($customer);
$customer_id = $this->customers_model->add($customer);
$appointment['id_users_customer'] = $customer_id;
$appointment['id'] = $this->appointments_model->add($appointment);
$appointment['hash'] = $this->appointments_model->get_value('hash', $appointment['id']);
$provider = $this->providers_model->get_row($appointment['id_users_provider']);
$service = $this->services_model->get_row($appointment['id_services']);
$company_settings = array(
'company_name' => $this->settings_model->get_setting('company_name'),
'company_link' => $this->settings_model->get_setting('company_link'),
'company_email' => $this->settings_model->get_setting('company_email')
);
// :: SYNCHRONIZE APPOINTMENT WITH PROVIDER'S GOOGLE CALENDAR
// The provider must have previously granted access to his google calendar account
// in order to sync the appointment.
try {
$google_sync = $this->providers_model->get_setting('google_sync',
$appointment['id_users_provider']);
if ($google_sync == TRUE) {
$google_token = json_decode($this->providers_model
->get_setting('google_token', $appointment['id_users_provider']));
$this->load->library('google_sync');
$this->google_sync->refresh_token($google_token->refresh_token);
if ($post_data['manage_mode'] === FALSE) {
// Add appointment to Google Calendar.
$google_event = $this->google_sync->add_appointment($appointment, $provider,
$service, $customer, $company_settings);
$appointment['id_google_calendar'] = $google_event->id;
$this->appointments_model->add($appointment);
} else {
// Update appointment to Google Calendar.
$appointment['id_google_calendar'] = $this->appointments_model
->get_value('id_google_calendar', $appointment['id']);
$this->google_sync->update_appointment($appointment, $provider,
$service, $customer, $company_settings);
}
}
} catch(Exception $exc) {
$view['exceptions'][] = $exc;
}
// :: SEND NOTIFICATION EMAILS TO BOTH CUSTOMER AND PROVIDER
try {
$this->load->library('Notifications');
$send_provider = $this->providers_model
->get_setting('notifications', $provider['id']);
if (!$post_data['manage_mode']) {
$customer_title = $this->lang->line('appointment_booked');
$customer_message = $this->lang->line('thank_your_for_appointment');
$customer_link = $this->config->item('base_url') . 'appointments/index/'
. $appointment['hash'];
$provider_title = $this->lang->line('appointment_added_to_your_plan');
$provider_message = $this->lang->line('appointment_link_description');
$provider_link = $this->config->item('base_url') . 'backend/index/'
. $appointment['hash'];
} else {
$customer_title = $this->lang->line('appointment_changes_saved');
$customer_message = '';
$customer_link = $this->config->item('base_url') . 'appointments/index/'
. $appointment['hash'];
$provider_title = $this->lang->line('appointment_details_changed');
$provider_message = '';
$provider_link = $this->config->item('base_url') . 'backend/index/'
. $appointment['hash'];
}
$this->notifications->send_appointment_details($appointment, $provider,
$service, $customer,$company_settings, $customer_title,
$customer_message, $customer_link, $customer['email']);
if ($send_provider == TRUE) {
$this->notifications->send_appointment_details($appointment, $provider,
$service, $customer, $company_settings, $provider_title,
$provider_message, $provider_link, $provider['email']);
}
} catch(Exception $exc) {
$view['exceptions'][] = $exc;
}
// :: LOAD THE BOOK SUCCESS VIEW
$view = array(
'appointment_data' => $appointment,
'provider_data' => $provider,
'service_data' => $service,
'company_name' => $company_settings['company_name']
);
} catch(Exception $exc) {
$view['exceptions'][] = $exc;
}
$this->load->view('appointments/book_success', $view);
}
}
/**
* Cancel an existing appointment.
*
* This method removes an appointment from the company's schedule.
* In order for the appointment to be deleted, the hash string must
* be provided. The customer can only cancel the appointment if the
* edit time period is not over yet.
*
* @param string $appointment_hash This is used to distinguish the
* appointment record.
* @param string $_POST['cancel_reason'] The text that describes why
* the customer cancelled the appointment.
*/
public function cancel($appointment_hash) {
try {
$this->load->model('appointments_model');
$this->load->model('providers_model');
$this->load->model('customers_model');
$this->load->model('services_model');
$this->load->model('settings_model');
// Check whether the appointment hash exists in the database.
$records = $this->appointments_model->get_batch(array('hash' => $appointment_hash));
if (count($records) == 0) {
throw new Exception('No record matches the provided hash.');
}
$appointment = $records[0];
$provider = $this->providers_model->get_row($appointment['id_users_provider']);
$customer = $this->customers_model->get_row($appointment['id_users_customer']);
$service = $this->services_model->get_row($appointment['id_services']);
$company_settings = array(
'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')
);
// :: DELETE APPOINTMENT RECORD FROM THE DATABASE.
if (!$this->appointments_model->delete($appointment['id'])) {
throw new Exception('Appointment could not be deleted from the database.');
}
// :: SYNC APPOINTMENT REMOVAL WITH GOOGLE CALENDAR
if ($appointment['id_google_calendar'] != NULL) {
try {
$google_sync = $this->providers_model->get_setting('google_sync',
$appointment['id_users_provider']);
if ($google_sync == TRUE) {
$google_token = json_decode($this->providers_model
->get_setting('google_token', $provider['id']));
$this->load->library('Google_Sync');
$this->google_sync->refresh_token($google_token->refresh_token);
$this->google_sync->delete_appointment($provider, $appointment['id_google_calendar']);
}
} catch(Exception $exc) {
$exceptions[] = $exc;
}
}
// :: SEND NOTIFICATION EMAILS TO CUSTOMER AND PROVIDER
try {
$this->load->library('Notifications');
$send_provider = $this->providers_model
->get_setting('notifications', $provider['id']);
if ($send_provider == TRUE) {
$this->notifications->send_delete_appointment($appointment, $provider,
$service, $customer, $company_settings, $provider['email'],
$_POST['cancel_reason']);
}
$this->notifications->send_delete_appointment($appointment, $provider,
$service, $customer, $company_settings, $customer['email'],
$_POST['cancel_reason']);
} catch(Exception $exc) {
$exceptions[] = $exc;
}
} catch(Exception $exc) {
// Display the error message to the customer.
$exceptions[] = $exc;
}
$view = array();
if (isset($exceptions)) {
$view['exceptions'] = $exceptions;
}
$this->load->view('appointments/cancel', $view);
}
/**
* [AJAX] Get the available appointment days for the given provider, service and timeframe.
*
* This method answers to an AJAX request. It calculates the available days
* for the given service, provider and timerange
*
* @param numeric $_POST['service_id'] The selected service's record id.
* @param numeric $_POST['provider_id'] The selected provider's record id.
* @param numeric $_POST['timeframe'] The number of days to look ahead. Default 30 days.
* @param numeric $_POST['service_duration'] The selected service duration in
* minutes.
* @param string $_POST['manage_mode'] Contains either 'true' or 'false' and determines
* the if current user is managing an already booked appointment or not.
* @param numeric $_POST['appointment_id'] If manage_mode is true,
* this should contain the current appointments id
* @return Returns a json object with the available hours.
*/
public function ajax_get_available_days() {
/* This uses ajax_get_available_hours even though it is really bad form,
so that code changes are kept to a minimum. This is really bad form however. Sorry. */
try {
$interval_oneday = new DateInterval( "P1D" );
$today = new DateTime("today");
$maxdays = (isset($_POST["timeframe"]) && is_int($_POST["timeframe"]))
? $_POST["timeframe"]
: 30;
$available_days = array();
for ($i = 0; $i < $maxdays; $i++) {
$_POST["selected_date"] = $today->format('d-m-Y');
// ARGH!
ob_start();
$this->ajax_get_available_hours();
$available_hours_string = ob_get_contents();
ob_end_clean();
$available_hours = json_decode($available_hours_string);
if (array_key_exists("exceptions", $available_hours)) {
continue;
} else {
if (count($available_hours)>0) {
$available_days[] = $today->format('d-m-Y');
}
}
$today->add($interval_oneday);
}
echo json_encode($available_days);
} catch(Exception $exc) {
echo json_encode(array(
'exceptions' => array(exceptionToJavaScript($exc))
));
}
}
/**
* [AJAX] Get the available appointment hours for the given date.
*
* This method answers to an AJAX request. It calculates the available hours
* for thegiven service, provider and date.
*
* @param numeric $_POST['service_id'] The selected service's record id.
* @param numeric $_POST['provider_id'] The selected provider's record id.
* @param string $_POST['selected_date'] The selected date of which the
* available hours we want to see.
* @param numeric $_POST['service_duration'] The selected service duration in
* minutes.
* @param string $$_POST['manage_mode'] Contains either 'true' or 'false' and determines
* the if current user is managing an already booked appointment or not.
* @return Returns a json object with the available hours.
*/
public function ajax_get_available_hours() {
$this->load->model('providers_model');
$this->load->model('appointments_model');
$this->load->model('settings_model');
try {
// If manage mode is TRUE then the following we should not consider the selected
// appointment when calculating the available time periods of the provider.
$exclude_appointments = ($_POST['manage_mode'] === 'true')
? array($_POST['appointment_id'])
: array();
$empty_periods = $this->get_provider_available_time_periods($_POST['provider_id'],
$_POST['selected_date'], $exclude_appointments);
// 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.
$available_hours = array();
foreach ($empty_periods as $period) {
$start_hour = new DateTime($_POST['selected_date'] . ' ' . $period['start']);
$end_hour = new DateTime($_POST['selected_date'] . ' ' . $period['end']);
$minutes = $start_hour->format('i');
if ($minutes % 15 != 0) {
// Change the start hour of the current space in order to be
// on of the following: 00, 15, 30, 45.
if ($minutes < 15) {
$start_hour->setTime($start_hour->format('H'), 15);
} else if ($minutes < 30) {
$start_hour->setTime($start_hour->format('H'), 30);
} else if ($minutes < 45) {
$start_hour->setTime($start_hour->format('H'), 45);
} else {
$start_hour->setTime($start_hour->format('H') + 1, 00);
}
}
$current_hour = $start_hour;
$diff = $current_hour->diff($end_hour);
while (($diff->h * 60 + $diff->i) >= intval($_POST['service_duration'])) {
$available_hours[] = $current_hour->format('H:i');
$current_hour->add(new DateInterval("PT15M"));
$diff = $current_hour->diff($end_hour);
}
}
// If the selected date is today, remove past hours. It is important
// include the timeout before booking that is set in the backoffice
// the system. Normally we might want the customer to book an appointment
// that is at least half or one hour from now. The setting is stored in
// minutes.
if (date('m/d/Y', strtotime($_POST['selected_date'])) == date('m/d/Y')) {
if ($_POST['manage_mode'] === 'true') {
$book_advance_timeout = 0;
} else {
$book_advance_timeout = $this->settings_model->get_setting('book_advance_timeout');
}
foreach($available_hours as $index => $value) {
$available_hour = strtotime($value);
$current_hour = strtotime('+' . $book_advance_timeout . ' minutes', strtotime('now'));
if ($available_hour <= $current_hour) {
unset($available_hours[$index]);
}
}
}
$available_hours = array_values($available_hours);
sort($available_hours, SORT_STRING );
$available_hours = array_values($available_hours);
echo json_encode($available_hours);
} catch(Exception $exc) {
echo json_encode(array(
'exceptions' => array(exceptionToJavaScript($exc))
));
}
}
/**
* Check whether the provider is still available in the selected appointment date.
*
* It might be times where two or more customers select the same appointment date and time.
* This shouldn't be allowed to happen, so one of the two customers will eventually get the
* prefered date and the other one will have to choose for another date. Use this method
* just before the customer confirms the appointment details. If the selected date was taken
* in the mean time, the customer must be prompted to select another time for his appointment.
*
* @param int $_POST['id_users_provider'] The selected provider's record id.
* @param int $_POST['id_services'] The selected service's record id.
* @param string $_POST['start_datetime'] This is a mysql formed string.
* @return bool Returns whether the selected datetime is still available.
*/
public function ajax_check_datetime_availability() {
try {
$this->load->model('services_model');
$service_duration = $this->services_model->get_value('duration', $_POST['id_services']);
$exclude_appointments = (isset($_POST['exclude_appointment_id']))
? array($_POST['exclude_appointment_id']) : array();
$available_periods = $this->get_provider_available_time_periods(
$_POST['id_users_provider'], $_POST['start_datetime'], $exclude_appointments);
$is_still_available = FALSE;
foreach($available_periods as $period) {
$appt_start = new DateTime($_POST['start_datetime']);
$appt_start = $appt_start->format('H:i');
$appt_end = new DateTime($_POST['start_datetime']);
$appt_end->add(new DateInterval('PT' . $service_duration . 'M'));
$appt_end = $appt_end->format('H:i');
$period_start = date('H:i', strtotime($period['start']));
$period_end = date('H:i', strtotime($period['end']));
if ($period_start <= $appt_start && $period_end >= $appt_end) {
$is_still_available = TRUE;
break;
}
}
echo json_encode($is_still_available);
} catch(Exception $exc) {
echo json_encode(array(
'exceptions' => array(exceptionToJavaScript($exc))
));
}
}
/**
* Get an array containing the free time periods (start - end) of a selected date.
*
* This method is very important because there are many cases where the system needs to
* know when a provider is avaible for an appointment. This method will return an array
* that belongs to the selected date and contains values that have the start and the end
* time of an available time period.
*
* @param numeric $provider_id The provider's record id.
* @param string $selected_date The date to be checked (MySQL formatted string).
* @param array $exclude_appointments This array contains the ids of the appointments that
* will not be taken into consideration when the available time periods are calculated.
* @return array Returns an array with the available time periods of the provider.
*/
private function get_provider_available_time_periods($provider_id, $selected_date,
$exclude_appointments = array()) {
$this->load->model('appointments_model');
$this->load->model('providers_model');
// Get the provider's working plan and reserved appointments.
$working_plan = json_decode($this->providers_model->get_setting('working_plan', $provider_id), true);
$where_clause = array(
//'DATE(start_datetime)' => date('Y-m-d', strtotime($selected_date)),
'id_users_provider' => $provider_id
);
$reserved_appointments = $this->appointments_model->get_batch($where_clause);
// Sometimes it might be necessary to not take into account some appointment records
// in order to display what the providers' available time periods would be without them.
foreach ($exclude_appointments as $excluded_id) {
foreach ($reserved_appointments as $index => $reserved) {
if ($reserved['id'] == $excluded_id) {
unset($reserved_appointments[$index]);
}
}
}
// Find the empty spaces on the plan. The first split between the plan is due to
// a break (if exist). After that every reserved appointment is considered to be
// a taken space in the plan.
$selected_date_working_plan = $working_plan[strtolower(date('l', strtotime($selected_date)))];
$available_periods_with_breaks = array();
if (isset($selected_date_working_plan['breaks'])) {
if (count($selected_date_working_plan['breaks'])) {
foreach($selected_date_working_plan['breaks'] as $index=>$break) {
// Split the working plan to available time periods that do not
// contain the breaks in them.
$last_break_index = $index - 1;
if (count($available_periods_with_breaks) === 0) {
$start_hour = $selected_date_working_plan['start'];
$end_hour = $break['start'];
} else {
$start_hour = $selected_date_working_plan['breaks'][$last_break_index]['end'];
$end_hour = $break['start'];
}
$available_periods_with_breaks[] = array(
'start' => $start_hour,
'end' => $end_hour
);
}
// Add the period from the last break to the end of the day.
$available_periods_with_breaks[] = array(
'start' => $selected_date_working_plan['breaks'][$index]['end'],
'end' => $selected_date_working_plan['end']
);
} else {
$available_periods_with_breaks[] = array(
'start' => $selected_date_working_plan['start'],
'end' => $selected_date_working_plan['end']
);
}
}
// Break the empty periods with the reserved appointments.
$available_periods_with_appointments = $available_periods_with_breaks;
foreach($reserved_appointments as $appointment) {
foreach($available_periods_with_appointments as $index => &$period) {
$a_start = strtotime($appointment['start_datetime']);
$a_end = strtotime($appointment['end_datetime']);
$p_start = strtotime($selected_date . ' ' . $period['start']);
$p_end = strtotime($selected_date . ' ' .$period['end']);
if ($a_start <= $p_start && $a_end <= $p_end && $a_end <= $p_start) {
// The appointment does not belong in this time period, so we
// will not change anything.
} else if ($a_start <= $p_start && $a_end <= $p_end && $a_end >= $p_start) {
// The appointment starts before the period and finishes somewhere inside.
// We will need to break this period and leave the available part.
$period['start'] = date('H:i', $a_end);
} else if ($a_start >= $p_start && $a_end <= $p_end) {
// The appointment is inside the time period, so we will split the period
// into two new others.
unset($available_periods_with_appointments[$index]);
$available_periods_with_appointments[] = array(
'start' => date('H:i', $p_start),
'end' => date('H:i', $a_start)
);
$available_periods_with_appointments[] = array(
'start' => date('H:i', $a_end),
'end' => date('H:i', $p_end)
);
} else if ($a_start >= $p_start && $a_end >= $p_start && $a_start <= $p_end) {
// The appointment starts in the period and finishes out of it. We will
// need to remove the time that is taken from the appointment.
$period['end'] = date('H:i', $a_start);
} else if ($a_start >= $p_start && $a_end >= $p_end && $a_start >= $p_end) {
// The appointment does not belong in the period so do not change anything.
} else if ($a_start <= $p_start && $a_end >= $p_end && $a_start <= $p_end) {
// The appointment is bigger than the period, so this period needs to be
// removed.
unset($available_periods_with_appointments[$index]);
}
}
}
return array_values($available_periods_with_appointments);
}
/**
* This method checks whether the application is installed.
*
* This method resides in this controller because the "index()" function will
* be the first to be launched after the files are on the server. NOTE that the
* "configuration.php" file must be already set because we won't be able to
* connect to the database otherwise.
*/
public function check_installation() {
try {
if (!$this->db->table_exists('ea_users')) {
// This is the first time the website is launched an the user needs to set
// the basic settings. Display the installation view page.
$view['base_url'] = $this->config->item('base_url');
$this->load->view('general/installation', $view);
return FALSE; // Do not display the book appointment view file.
} else {
return TRUE; // Application installed, continue ...
}
} catch(Exception $exc) {
echo $exc->getTrace();
}
}
/**
* Installs Easy!Appointments on server.
*
* @param array $_POST['admin'] Contains the initial admin user data. System needs at least
* one admin user to work.
* @param array $_POST['company'] Contains the basic company data.
*/
public function ajax_install() {
try {
// Create E!A database structure.
$file_contents = file_get_contents($this->config->item('base_url') . 'assets/sql/structure.sql');
$sql_queries = explode(';', $file_contents);
array_pop($sql_queries);
foreach($sql_queries as $query) {
$this->db->query($query);
}
// Insert admin
$this->load->model('admins_model');
$admin = json_decode($_POST['admin'], true);
$admin['settings']['username'] = $admin['username'];
$admin['settings']['password'] = $admin['password'];
unset($admin['username'], $admin['password']);
$admin['id'] = $this->admins_model->add($admin);
$this->load->library('session');
$this->session->set_userdata('user_id', $admin['id']);
$this->session->set_userdata('user_email', $admin['email']);
$this->session->set_userdata('role_slug', DB_SLUG_ADMIN);
$this->session->set_userdata('username', $admin['settings']['username']);
// Save company settings
$this->load->model('settings_model');
$company = json_decode($_POST['company'], true);
$this->settings_model->set_setting('company_name', $company['company_name']);
$this->settings_model->set_setting('company_email', $company['company_email']);
$this->settings_model->set_setting('company_link', $company['company_link']);
// Try to send a notification email for the new installation.
// IMPORTANT: THIS WILL ONLY BE USED TO TRACK THE INSTALLATION NUMBER AND
// NO PERSONAL DATA WILL BE USED FOR OTHER CAUSE.
try {
$this->load->library('notifications');
$this->notifications->send_new_installation($company['company_name'],
$company['company_email'], $company['company_link']);
} catch(Exception $exc) {
// Well, I guess we'll never know ...
}
echo json_encode(AJAX_SUCCESS);
} catch (Exception $exc) {
echo json_encode(array(
'exceptions' => array(exceptionToJavaScript($exc))
));
}
}
}
/* End of file appointments.php */
/* Location: ./application/controllers/appointments.php */
/**
* This namespace contains functions that implement the book appointment page
* functionality. Once the initialize() method is called the page is fully
* functional and can serve the appointment booking process.
*
* @namespace FrontendBook
*/
var FrontendBook = {
/**
* Determines the functionality of the page.
*
* @type {bool}
*/
manageMode: false,
/*
* Number of days to look ahead.
*/
timeframe: 30,
/**
* This method initializes the book appointment page.
*
* @param {bool} bindEventHandlers (OPTIONAL) Determines whether the default
* event handlers will be binded to the dom elements.
* @param {bool} manageMode (OPTIONAL) Determines whether the customer is going
* to make changes to an existing appointment rather than booking a new one.
*/
initialize: function(bindEventHandlers, manageMode) {
if (bindEventHandlers === undefined) {
bindEventHandlers = true; // Default Value
}
if (manageMode === undefined) {
manageMode = false; // Default Value
}
FrontendBook.manageMode = manageMode;
// Initialize page's components (tooltips, datepickers etc).
$('.book-step').qtip({
position: {
my: 'top center',
at: 'bottom center'
},
style: {
classes: 'qtip-green qtip-shadow custom-qtip'
}
});
$('#select-date').datepicker({
dateFormat: 'dd-mm-yy',
firstDay: 1, // Monday
minDate: 0,
maxDate: "+" + FrontendBook.timeframe + "d",
defaultDate: Date.today(),
availableDays: [], // by default empty.
dayNames: [EALang['sunday'], EALang['monday'], EALang['tuesday'], EALang['wednesday'], EALang['thursday'], EALang['friday'], EALang['saturday']],
dayNamesShort: [EALang['sunday'].substr(0,3), EALang['monday'].substr(0,3),
EALang['tuesday'].substr(0,3), EALang['wednesday'].substr(0,3),
EALang['thursday'].substr(0,3), EALang['friday'].substr(0,3),
EALang['saturday'].substr(0,3)],
dayNamesMin: [EALang['sunday'].substr(0,2), EALang['monday'].substr(0,2),
EALang['tuesday'].substr(0,2), EALang['wednesday'].substr(0,2),
EALang['thursday'].substr(0,2), EALang['friday'].substr(0,2),
EALang['saturday'].substr(0,2)],
monthNames: [EALang['january'], EALang['february'], EALang['march'], EALang['april'],
EALang['may'], EALang['june'], EALang['july'], EALang['august'], EALang['september'],
EALang['october'], EALang['november'], EALang['december']],
prevText: EALang['previous'],
nextText: EALang['next'],
currentText: EALang['now'],
closeText: EALang['close'],
disabled: true,
beforeShowDay: function(date) {
var date_string = date.toString("dd-MM-yyyy");
//console.log("date_sring:" + date_string);
console.log(date_string + ":" + $('#select-date').datepicker("option","availableDays").indexOf(date_string));
if ($('#select-date').datepicker("option","availableDays").indexOf(date_string) >= 0)
return [true];
return [false,"",EALang['no_available_hours']];
},
onSelect: function(dateText, instance) {
FrontendBook.getAvailableHours(dateText);
FrontendBook.updateConfirmFrame();
}
});
// Bind the event handlers (might not be necessary every time
// we use this class).
if (bindEventHandlers) {
FrontendBook.bindEventHandlers();
}
// If the manage mode is true, the appointments data should be
// loaded by default.
if (FrontendBook.manageMode) {
FrontendBook.applyAppointmentData(GlobalVariables.appointmentData,
GlobalVariables.providerData, GlobalVariables.customerData);
} else {
$('#select-service').trigger('change'); // Load the available hours.
}
},
/**
* This method binds the necessary event handlers for the book
* appointments page.
*/
bindEventHandlers: function() {
/**
* Event: Selected Provider "Changed"
*
* Whenever the provider changes the available appointment
* date - time periods must be updated.
*/
$('#select-provider').change(function() {
FrontendBook.updateCalendar();
FrontendBook.getAvailableHours(Date.today().toString('dd-MM-yyyy'));
FrontendBook.updateConfirmFrame();
});
/**
* Event: Selected Service "Changed"
*
* When the user clicks on a service, its available providers should
* become visible.
*/
$('#select-service').change(function() {
var currServiceId = $('#select-service').val();
$('#select-provider').empty();
$.each(GlobalVariables.availableProviders, function(indexProvider, provider) {
$.each(provider['services'], function(indexService, serviceId) {
// If the current provider is able to provide the selected service,
// add him to the listbox.
if (serviceId == currServiceId) {
var optionHtml = '<option value="' + provider['id'] + '">'
+ provider['first_name'] + ' ' + provider['last_name']
+ '</option>';
$('#select-provider').append(optionHtml);
}
});
});
FrontendBook.updateCalendar();
FrontendBook.getAvailableHours($('#select-date').val());
FrontendBook.updateConfirmFrame();
FrontendBook.updateServiceDescription($('#select-service').val(), $('#service-description'));
});
/**
* Event: Next Step Button "Clicked"
*
* This handler is triggered every time the user pressed the
* "next" button on the book wizard. Some special tasks might
* be perfomed, depending the current wizard step.
*/
$('.button-next').click(function() {
// If we are on the 2nd tab then the user should have an appointment hour
// selected.
if ($(this).attr('data-step_index') === '2') {
if ($('.selected-hour').length == 0) {
if ($('#select-hour-prompt').length == 0) {
$('#available-hours').append('<br><br>'
+ '<strong id="select-hour-prompt" class="text-error">'
+ EALang['appointment_hour_missing']
+ '</strong>');
}
return;
}
}
// If we are on the 3rd tab then we will need to validate the user's
// input before proceeding to the next step.
if ($(this).attr('data-step_index') === '3') {
if (!FrontendBook.validateCustomerForm()) {
return; // Validation failed, do not continue.
} else {
FrontendBook.updateConfirmFrame();
}
}
// Display the next step tab (uses jquery animation effect).
var nextTabIndex = parseInt($(this).attr('data-step_index')) + 1;
$(this).parents().eq(1).hide('fade', function() {
$('.active-step').removeClass('active-step');
$('#step-' + nextTabIndex).addClass('active-step');
$('#wizard-frame-' + nextTabIndex).show('fade');
});
});
/**
* Event: Back Step Button "Clicked"
*
* This handler is triggered every time the user pressed the
* "back" button on the book wizard.
*/
$('.button-back').click(function() {
var prevTabIndex = parseInt($(this).attr('data-step_index')) - 1;
$(this).parents().eq(1).hide('fade', function() {
$('.active-step').removeClass('active-step');
$('#step-' + prevTabIndex).addClass('active-step');
$('#wizard-frame-' + prevTabIndex).show('fade');
});
});
/**
* Event: Available Hour "Click"
*
* Triggered whenever the user clicks on an available hour
* for his appointment.
*/
$('#available-hours').on('click', '.available-hour', function() {
$('.selected-hour').removeClass('selected-hour');
$(this).addClass('selected-hour');
FrontendBook.updateConfirmFrame();
});
if (FrontendBook.manageMode) {
/**
* Event: Cancel Appointment Button "Click"
*
* When the user clicks the "Cancel" button this form is going to
* be submitted. We need the user to confirm this action because
* once the appointment is cancelled, it will be delete from the
* database.
*/
$('#cancel-appointment').click(function(event) {
var dialogButtons = {};
dialogButtons['OK'] = function() {
if ($('#cancel-reason').val() === '') {
$('#cancel-reason').css('border', '2px solid red');
return;
}
$('#cancel-appointment-form textarea').val($('#cancel-reason').val());
$('#cancel-appointment-form').submit();
};
dialogButtons[EALang['cancel']] = function() {
$('#message_box').dialog('close');
};
GeneralFunctions.displayMessageBox(EALang['cancel_appointment_title'],
EALang['write_appointment_removal_reason'], dialogButtons);
$('#message_box').append('<textarea id="cancel-reason" rows="3"></textarea>');
$('#cancel-reason').css('width', '353px');
return false;
});
}
/**
* Event: Book Appointment Form "Submit"
*
* Before the form is submitted to the server we need to make sure that
* in the meantime the selected appointment date/time wasn't reserved by
* another customer or event.
*/
$('#book-appointment-submit').click(function(event) {
var formData = jQuery.parseJSON($('input[name="post_data"]').val());
var postData = {
'id_users_provider': formData['appointment']['id_users_provider'],
'id_services': formData['appointment']['id_services'],
'start_datetime': formData['appointment']['start_datetime'],
};
if (GlobalVariables.manageMode) {
postData.exclude_appointment_id = GlobalVariables.appointmentData.id;
}
var postUrl = GlobalVariables.baseUrl + 'appointments/ajax_check_datetime_availability';
$.post(postUrl, postData, function(response) {
////////////////////////////////////////////////////////////////////////
console.log('Check Date/Time Availability Post Response :', response);
////////////////////////////////////////////////////////////////////////
if (response.exceptions) {
response.exceptions = GeneralFunctions.parseExceptions(response.exceptions);
GeneralFunctions.displayMessageBox('Unexpected Issues', 'Unfortunately '
+ 'the check appointment time availability could not be completed. '
+ 'The following issues occurred:');
$('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions));
return false;
}
if (response === true) {
$('#book-appointment-form').submit();
} else {
GeneralFunctions.displayMessageBox('Appointment Hour Taken', 'Unfortunately '
+ 'the selected appointment hour is not available anymore. Please select '
+ 'another hour.');
FrontendBook.getAvailableHours($('#select-date').val());
}
}, 'json');
});
},
updateCalendar: function() {
// Find the selected service duration (it is going to
// be send within the "postData" object).
var selServiceDuration = 15; // Default value of duration (in minutes).
$.each(GlobalVariables.availableServices, function(index, service) {
if (service['id'] == $('#select-service').val()) {
selServiceDuration = service['duration'];
}
});
// If the manage mode is true then the appointment's start
// date should return as available too.
var appointmentId = (FrontendBook.manageMode)
? GlobalVariables.appointmentData['id'] : undefined;
var postData = {
'service_id': $('#select-service').val(),
'provider_id': $('#select-provider').val(),
'service_duration': selServiceDuration,
'manage_mode': FrontendBook.manageMode,
'appointment_id': appointmentId,
'timeframe': FrontendBook.timeframe
};
$('#select-date').datepicker("option","disabled",true);
$('#select-date').datepicker("refresh");
// Make ajax post request and get the available hours.
var ajaxurl = GlobalVariables.baseUrl + 'appointments/ajax_get_available_days';
jQuery.post(ajaxurl, postData, function(response) {
///////////////////////////////////////////////////////////////
console.log('Get Available Days JSON Response:', response);
///////////////////////////////////////////////////////////////
if (!GeneralFunctions.handleAjaxExceptions(response)) return;
// The response contains the available days for the selected provider,
// service and timeframe. save that info in the timepicker and refresh it.
$('#select-date').datepicker("option","availableDays", response);
$('#select-date').datepicker("option","disabled", false);
$('#select-date').datepicker("refresh");
}, 'json');
},
/**
* This function makes an ajax call and returns the available
* hours for the selected service, provider and date.
*
* @param {string} selDate The selected date of which the available
* hours we need to receive.
*/
getAvailableHours: function(selDate) {
$('#available-hours').empty();
// Find the selected service duration (it is going to
// be send within the "postData" object).
var selServiceDuration = 15; // Default value of duration (in minutes).
$.each(GlobalVariables.availableServices, function(index, service) {
if (service['id'] == $('#select-service').val()) {
selServiceDuration = service['duration'];
}
});
// If the manage mode is true then the appointment's start
// date should return as available too.
var appointmentId = (FrontendBook.manageMode)
? GlobalVariables.appointmentData['id'] : undefined;
var postData = {
'service_id': $('#select-service').val(),
'provider_id': $('#select-provider').val(),
'selected_date': selDate,
'service_duration': selServiceDuration,
'manage_mode': FrontendBook.manageMode,
'appointment_id': appointmentId
};
// Make ajax post request and get the available hours.
var ajaxurl = GlobalVariables.baseUrl + 'appointments/ajax_get_available_hours';
jQuery.post(ajaxurl, postData, function(response) {
///////////////////////////////////////////////////////////////
console.log('Get Available Hours JSON Response:', response);
///////////////////////////////////////////////////////////////
if (!GeneralFunctions.handleAjaxExceptions(response)) return;
// The response contains the available hours for the selected provider and
// service. Fill the available hours div with response data.
if (response.length > 0) {
var currColumn = 1;
$('#available-hours').html('<div style="width:50px; float:left;"></div>');
$.each(response, function(index, availableHour) {
if ((currColumn * 10) < (index + 1)) {
currColumn++;
$('#available-hours').append('<div style="width:50px; float:left;"></div>');
}
$('#available-hours div:eq(' + (currColumn - 1) + ')').append(
'<span class="available-hour">' + availableHour + '</span><br/>');
});
if (FrontendBook.manageMode) {
// Set the appointment's start time as the default selection.
$('.available-hour').removeClass('selected-hour');
$('.available-hour').filter(function() {
return $(this).text() === Date.parseExact(
GlobalVariables.appointmentData['start_datetime'],
'yyyy-MM-dd HH:mm:ss').toString('HH:mm');
}).addClass('selected-hour');
} else {
// Set the first available hour as the default selection.
$('.available-hour:eq(0)').addClass('selected-hour');
}
FrontendBook.updateConfirmFrame();
} else {
$('#available-hours').text(EALang['no_available_hours']);
}
}, 'json');
},
/**
* This function validates the customer's data input. The user cannot contiue
* without passing all the validation checks.
*
* @return {bool} Returns the validation result.
*/
validateCustomerForm: function() {
$('#wizard-frame-3 input').css('border', '');
try {
// Validate required fields.
var missingRequiredField = false;
$('.required').each(function() {
if ($(this).val() == '') {
$(this).css('border', '2px solid red');
missingRequiredField = true;
}
});
if (missingRequiredField) {
throw EALang['fields_are_required'];
}
// Validate email address.
if (!GeneralFunctions.validateEmail($('#email').val())) {
$('#email').css('border', '2px solid red');
throw EALang['invalid_email'];
}
return true;
} catch(exc) {
$('#form-message').text(exc);
return false;
}
},
/**
* Every time this function is executed, it updates the confirmation
* page with the latest customer settigns and input for the appointment
* booking.
*/
updateConfirmFrame: function() {
// Appointment Details
var selectedDate = $('#select-date').datepicker('getDate');
if (selectedDate !== null) {
selectedDate = Date.parse(selectedDate).toString('dd/MM/yyyy');
}
var selServiceId = $('#select-service').val();
var servicePrice, serviceCurrency;
$.each(GlobalVariables.availableServices, function(index, service) {
if (service.id == selServiceId) {
servicePrice = '<br>' + service.price;
serviceCurrency = service.currency;
return false; // break loop
}
});
$('#appointment-details').html(
'<h4>' + $('#select-service option:selected').text() + '</h4>' +
'<p>'
+ '<strong class="text-info">'
+ $('#select-provider option:selected').text() + '<br>'
+ selectedDate + ' ' + $('.selected-hour').text()
+ servicePrice + ' ' + serviceCurrency
+ '</strong>' +
'</p>'
);
// Customer Details
$('#customer-details').html(
'<h4>' + $('#first-name').val() + ' ' + $('#last-name').val() + '</h4>' +
'<p>' +
EALang['phone'] + ': ' + $('#phone-number').val() +
'<br/>' +
EALang['email'] + ': ' + $('#email').val() +
'<br/>' +
EALang['address'] + ': ' + $('#address').val() +
'<br/>' +
EALang['city'] + ': ' + $('#city').val() +
'<br/>' +
EALang['zip_code'] + ': ' + $('#zip-code').val() +
'</p>'
);
// Update appointment form data for submission to server when the user confirms
// the appointment.
var postData = new Object();
postData['customer'] = {
'last_name': $('#last-name').val(),
'first_name': $('#first-name').val(),
'email': $('#email').val(),
'phone_number': $('#phone-number').val(),
'address': $('#address').val(),
'city': $('#city').val(),
'zip_code': $('#zip-code').val()
};
postData['appointment'] = {
'start_datetime': $('#select-date').datepicker('getDate').toString('yyyy-MM-dd')
+ ' ' + $('.selected-hour').text() + ':00',
'end_datetime': FrontendBook.calcEndDatetime(),
'notes': $('#notes').val(),
'is_unavailable': false,
'id_users_provider': $('#select-provider').val(),
'id_services': $('#select-service').val()
};
postData['manage_mode'] = FrontendBook.manageMode;
if (FrontendBook.manageMode) {
postData['appointment']['id'] = GlobalVariables.appointmentData['id'];
postData['customer']['id'] = GlobalVariables.customerData['id'];
}
$('input[name="post_data"]').val(JSON.stringify(postData));
},
/**
* This method calculates the end datetime of the current appointment.
* End datetime is depending on the service and start datetime fieldss.
*
* @return {string} Returns the end datetime in string format.
*/
calcEndDatetime: function() {
// Find selected service duration.
var selServiceDuration = undefined;
$.each(GlobalVariables.availableServices, function(index, service) {
if (service.id == $('#select-service').val()) {
selServiceDuration = service.duration;
return false; // Stop searching ...
}
});
// Add the duration to the start datetime.
var startDatetime = $('#select-date').datepicker('getDate').toString('dd-MM-yyyy')
+ ' ' + $('.selected-hour').text();
startDatetime = Date.parseExact(startDatetime, 'dd-MM-yyyy HH:mm');
var endDatetime = undefined;
if (selServiceDuration !== undefined && startDatetime !== null) {
endDatetime = startDatetime.add({ 'minutes' : parseInt(selServiceDuration) });
} else {
endDatetime = new Date();
}
return endDatetime.toString('yyyy-MM-dd HH:mm:ss');
},
/**
* This method applies the appointment's data to the wizard so
* that the user can start making changes on an existing record.
*
* @param {object} appointment Selected appointment's data.
* @param {object} provider Selected provider's data.
* @param {object} customer Selected customer's data.
* @returns {bool} Returns the operation result.
*/
applyAppointmentData: function(appointment, provider, customer) {
try {
// Select Service & Provider
$('#select-service').val(appointment['id_services']).trigger('change');
$('#select-provider').val(appointment['id_users_provider']);
// Set Appointment Date
$('#select-date').datepicker('setDate',
Date.parseExact(appointment['start_datetime'], 'yyyy-MM-dd HH:mm:ss'));
FrontendBook.getAvailableHours($('#select-date').val());
// Apply Customer's Data
$('#last-name').val(customer['last_name']);
$('#first-name').val(customer['first_name']);
$('#email').val(customer['email']);
$('#phone-number').val(customer['phone_number']);
$('#address').val(customer['address']);
$('#city').val(customer['city']);
$('#zip-code').val(customer['zip_code']);
var appointmentNotes = (appointment['notes'] !== null)
? appointment['notes'] : '';
$('#notes').val(appointmentNotes);
FrontendBook.updateConfirmFrame();
return true;
} catch(exc) {
console.log(exc); // log exception
return false;
}
},
/**
* This method updates a div's html content with a brief description of the
* user selected service (only if available in db). This is usefull for the
* customers upon selecting the correct service.
*
* @param {int} serviceId The selected service record id.
* @param {object} $div The destination div jquery object (e.g. provide $('#div-id')
* object as value).
*/
updateServiceDescription: function(serviceId, $div) {
var html = '';
$.each(GlobalVariables.availableServices, function(index, service) {
if (service.id == serviceId) { // Just found the service.
html = '<strong>' + service.name + ' </strong>';
if (service.description != '' && service.description != null) {
html += '<br>' + service.description + '<br>';
}
if (service.duration != '' && service.duration != null) {
html += '[' + EALang['duration'] + ' ' + service.duration
+ ' ' + EALang['minutes'] + '] ';
}
if (service.price != '' && service.price != null) {
html += '[' + EALang['price'] + ' ' + service.price + ' ' + service.currency + ']';
}
html += '<br>';
return false;
}
});
$div.html(html);
if (html != '') {
$div.show();
} else {
$div.hide();
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment