diff --git a/src/application/controllers/appointments.php b/src/application/controllers/appointments.php index 40d26185..ed510221 100644 --- a/src/application/controllers/appointments.php +++ b/src/application/controllers/appointments.php @@ -30,29 +30,25 @@ class Appointments extends CI_Controller { // Load the appointments data and enable the manage mode of the page. $manage_mode = TRUE; - $appointments_results = $this->appointments_model - ->get_batch(array('hash' => $appointment_hash)); + $results = $this->appointments_model->get_batch(array('hash' => $appointment_hash)); - if (count($appointments_results) === 0) { + if (count($results) === 0) { // The requested appointment doesn't exist in the database. Display // a message to the customer. - $view_data = array( + $view = array( 'message_title' => 'Appointment Not Found!', 'message_text' => 'The appointment you requested does not exist in ' . 'the system database anymore.', 'message_icon' => $this->config->item('base_url') . 'assets/images/error.png' ); - $this->load->view('appointments/message', $view_data); + $this->load->view('appointments/message', $view); return; } - $appointment = $appointments_results[0]; - - $provider = $this->providers_model - ->get_row($appointment['id_users_provider']); - $customer = $this->customers_model - ->get_row($appointment['id_users_customer']); + $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 @@ -64,7 +60,7 @@ class Appointments extends CI_Controller { } // Load the book appointment view. - $view_data = array ( + $view = array ( 'available_services' => $available_services, 'available_providers' => $available_providers, 'company_name' => $company_name, @@ -75,10 +71,10 @@ class Appointments extends CI_Controller { ); } catch(Exception $exc) { - $view_data['exceptions'][] = $exc; + $view['exceptions'][] = $exc; } - $this->load->view('appointments/book', $view_data); + $this->load->view('appointments/book', $view); } else { // The page is a post-back. Register the appointment and send notification emails @@ -135,7 +131,7 @@ class Appointments extends CI_Controller { } } } catch(Exception $exc) { - $view_data['exceptions'][] = $exc; + $view['exceptions'][] = $exc; } // :: SEND NOTIFICATION EMAILS TO BOTH CUSTOMER AND PROVIDER @@ -175,11 +171,11 @@ class Appointments extends CI_Controller { $service, $customer, $company_settings, $provider_title, $provider_message, $provider_link, $provider['email']); } catch(Exception $exc) { - $view_data['exceptions'][] = $exc; + $view['exceptions'][] = $exc; } // :: LOAD THE BOOK SUCCESS VIEW - $view_data = array( + $view = array( 'appointment_data' => $appointment, 'provider_data' => $provider, 'service_data' => $service, @@ -187,10 +183,10 @@ class Appointments extends CI_Controller { ); } catch(Exception $exc) { - $view_data['exceptions'][] = $exc; + $view['exceptions'][] = $exc; } - $this->load->view('appointments/book_success', $view_data); + $this->load->view('appointments/book_success', $view); } } @@ -273,10 +269,10 @@ class Appointments extends CI_Controller { } if (isset($exceptions)) { - $view_data['exceptions'] = $exceptions; + $view['exceptions'] = $exceptions; } - $this->load->view('appointments/cancel', $view_data); + $this->load->view('appointments/cancel', $view); } /** @@ -451,12 +447,12 @@ class Appointments extends CI_Controller { $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); + $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 + 'DATE(start_datetime)' => date('Y-m-d', strtotime($selected_date)), + 'id_users_provider' => $provider_id ); $reserved_appointments = $this->appointments_model->get_batch($where_clause); @@ -474,8 +470,7 @@ class Appointments extends CI_Controller { // 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)))]; + $selected_date_working_plan = $working_plan[strtolower(date('l', strtotime($selected_date)))]; $available_periods_with_breaks = array(); if (isset($selected_date_working_plan['breaks'])) { @@ -486,22 +481,22 @@ class Appointments extends CI_Controller { if (count($available_periods_with_breaks) === 0) { $start_hour = $selected_date_working_plan['start']; - $end_hour = $break['start']; + $end_hour = $break['start']; } else { $start_hour = $selected_date_working_plan['breaks'][$last_break_index]['end']; - $end_hour = $break['start']; + $end_hour = $break['start']; } $available_periods_with_breaks[] = array( 'start' => $start_hour, - 'end' => $end_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'] + 'end' => $selected_date_working_plan['end'] ); } @@ -512,19 +507,19 @@ class Appointments extends CI_Controller { foreach($available_periods_with_breaks as $period) { - foreach($reserved_appointments as $index=>$excluded_appointment) { - $appointment_start = date('H:i', strtotime($excluded_appointment['start_datetime'])); - $appointment_end = date('H:i', strtotime($excluded_appointment['end_datetime'])); - $period_start = date('H:i', strtotime($period['start'])); - $period_end = date('H:i', strtotime($period['end'])); + foreach($reserved_appointments as $index=>$appointment) { + $appointment_start = date('H:i', strtotime($appointment['start_datetime'])); + $appointment_end = date('H:i', strtotime($appointment['end_datetime'])); + $period_start = date('H:i', strtotime($period['start'])); + $period_end = date('H:i', strtotime($period['end'])); - if ($period_start < $appointment_start && $period_end > $appointment_end) { + if ($period_start <= $appointment_start && $period_end >= $appointment_end) { // We need to check whether another appointment fits in the current // time period. If this happens, then we need to consider the whole // appointment time as one, because the provider will not be available. foreach ($reserved_appointments as $tmp_appointment) { - $appt_start = date('H:i', strtotime($tmp_appointment['start_datetime'])); - $appt_end = date('H:i', strtotime($tmp_appointment['end_datetime'])); + $appt_start = date('H:i', strtotime($tmp_appointment['start_datetime'])); + $appt_end = date('H:i', strtotime($tmp_appointment['end_datetime'])); if ($period_start < $appt_start && $period_end > $appt_end) { if ($appointment_start > $appt_start) { diff --git a/src/application/controllers/backend.php b/src/application/controllers/backend.php index a548d1ad..33be1ed4 100644 --- a/src/application/controllers/backend.php +++ b/src/application/controllers/backend.php @@ -8,23 +8,34 @@ class Backend extends CI_Controller { * view this page which displays a calendar with the events of the selected * provider or service. If a user has more priviledges he will see more menus * at the top of the page. + * + * @param string $appointment_hash If given, the appointment edit dialog will + * appear when the page loads. */ - public function index() { + public function index($appointment_hash = '') { // @task Require user to be logged in the application. + $this->load->model('appointments_model'); $this->load->model('providers_model'); $this->load->model('services_model'); $this->load->model('settings_model'); - $view_data['base_url'] = $this->config->item('base_url'); - $view_data['book_advance_timeout'] = $this->settings_model->get_setting('book_advance_timeout'); - $view_data['company_name'] = $this->settings_model->get_setting('company_name'); - $view_data['available_providers'] = $this->providers_model->get_available_providers(); - $view_data['available_services'] = $this->services_model->get_available_services(); + $view['base_url'] = $this->config->item('base_url'); + $view['book_advance_timeout'] = $this->settings_model->get_setting('book_advance_timeout'); + $view['company_name'] = $this->settings_model->get_setting('company_name'); + $view['available_providers'] = $this->providers_model->get_available_providers(); + $view['available_services'] = $this->services_model->get_available_services(); - $this->load->view('backend/header', $view_data); - $this->load->view('backend/calendar', $view_data); - $this->load->view('backend/footer', $view_data); + if ($appointment_hash != '') { + $results = $this->appointments_model->get_batch(array('hash' => $appointment_hash)); + $view['edit_appointment'] = $results[0]; // This will display the appointment edit dialog on page load. + } else { + $view['edit_appointment'] = NULL; + } + + $this->load->view('backend/header', $view); + $this->load->view('backend/calendar', $view); + $this->load->view('backend/footer', $view); } /** @@ -40,15 +51,15 @@ class Backend extends CI_Controller { $this->load->model('services_model'); $this->load->model('settings_model'); - $view_data['base_url'] = $this->config->item('base_url'); - $view_data['company_name'] = $this->settings_model->get_setting('company_name'); - $view_data['customers'] = $this->customers_model->get_batch(); - $view_data['available_providers'] = $this->providers_model->get_available_providers(); - $view_data['available_services'] = $this->services_model->get_available_services(); + $view['base_url'] = $this->config->item('base_url'); + $view['company_name'] = $this->settings_model->get_setting('company_name'); + $view['customers'] = $this->customers_model->get_batch(); + $view['available_providers'] = $this->providers_model->get_available_providers(); + $view['available_services'] = $this->services_model->get_available_services(); - $this->load->view('backend/header', $view_data); - $this->load->view('backend/customers', $view_data); - $this->load->view('backend/footer', $view_data); + $this->load->view('backend/header', $view); + $this->load->view('backend/customers', $view); + $this->load->view('backend/footer', $view); } public function services() { diff --git a/src/application/controllers/backend_api.php b/src/application/controllers/backend_api.php index 656e44e6..42476282 100644 --- a/src/application/controllers/backend_api.php +++ b/src/application/controllers/backend_api.php @@ -7,12 +7,11 @@ class Backend_api extends CI_Controller { /** * [AJAX] Get the registered appointments for the given date period and record. * - * This method returns the database appointments for the user selected date - * period and record type (provider or service). + * This method returns the database appointments and unavailable periods for the + * user selected date period and record type (provider or service). * * @param {numeric} $_POST['record_id'] Selected record id. - * @param {string} $_POST['filter_type'] Could be either FILTER_TYPE_PROVIDER - * or FILTER_TYPE_SERVICE. + * @param {string} $_POST['filter_type'] Could be either FILTER_TYPE_PROVIDER or FILTER_TYPE_SERVICE. * @param {string} $_POST['start_date'] The user selected start date. * @param {string} $_POST['end_date'] The user selected end date. */ @@ -29,23 +28,35 @@ class Backend_api extends CI_Controller { $where_id = 'id_services'; } + // Get appointments $where_clause = array( $where_id => $_POST['record_id'], 'start_datetime >=' => $_POST['start_date'], - 'end_datetime <=' => $_POST['end_date'] + 'end_datetime <=' => $_POST['end_date'], + 'is_unavailable' => FALSE ); + + $response['appointments'] = $this->appointments_model->get_batch($where_clause); - $appointments = $this->appointments_model->get_batch($where_clause); - - foreach($appointments as &$appointment) { - if ($appointment['is_unavailable'] == FALSE) { - $appointment['provider'] = $this->providers_model->get_row($appointment['id_users_provider']); - $appointment['service'] = $this->services_model->get_row($appointment['id_services']); - $appointment['customer'] = $this->customers_model->get_row($appointment['id_users_customer']); - } + foreach($response['appointments'] as &$appointment) { + $appointment['provider'] = $this->providers_model->get_row($appointment['id_users_provider']); + $appointment['service'] = $this->services_model->get_row($appointment['id_services']); + $appointment['customer'] = $this->customers_model->get_row($appointment['id_users_customer']); } - - echo json_encode($appointments); + + // Get unavailable periods (only for provider). + if ($_POST['filter_type'] == FILTER_TYPE_PROVIDER) { + $where_clause = array( + $where_id => $_POST['record_id'], + 'start_datetime >=' => $_POST['start_date'], + 'end_datetime <=' => $_POST['end_date'], + 'is_unavailable' => TRUE + ); + + $response['unavailables'] = $this->appointments_model->get_batch($where_clause); + } + + echo json_encode($response); } catch(Exception $exc) { echo json_encode(array( @@ -205,10 +216,10 @@ class Backend_api extends CI_Controller { $this->load->model('services_model'); $this->load->model('settings_model'); - $appointment_data = $this->appointments_model->get_row($_POST['appointment_id']); - $provider_data = $this->providers_model->get_row($appointment_data['id_users_provider']); - $customer_data = $this->customers_model->get_row($appointment_data['id_users_customer']); - $service_data = $this->services_model->get_row($appointment_data['id_services']); + $appointment = $this->appointments_model->get_row($_POST['appointment_id']); + $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'), @@ -220,16 +231,16 @@ class Backend_api extends CI_Controller { $this->appointments_model->delete($_POST['appointment_id']); // :: SYNC DELETE WITH GOOGLE CALENDAR - if ($appointment_data['id_google_calendar'] != NULL) { + if ($appointment['id_google_calendar'] != NULL) { try { - $google_sync = $this->providers_model->get_setting('google_sync', $provider_data['id']); + $google_sync = $this->providers_model->get_setting('google_sync', $provider['id']); if ($google_sync == TRUE) { $google_token = json_decode($this->providers_model - ->get_setting('google_token', $provider_data['id'])); + ->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($appointment_data['id_google_calendar']); + $this->google_sync->delete_appointment($appointment['id_google_calendar']); } } catch(Exception $exc) { $warnings[] = exceptionToJavascript($exc); @@ -239,11 +250,11 @@ class Backend_api extends CI_Controller { // :: SEND NOTIFICATION EMAILS TO PROVIDER AND CUSTOMER try { $this->load->library('Notifications'); - $this->notifications->send_delete_appointment($appointment_data, $provider_data, - $service_data, $customer_data, $company_settings, $provider_data['email'], + $this->notifications->send_delete_appointment($appointment, $provider, + $service, $customer, $company_settings, $provider['email'], $_POST['delete_reason']); - $this->notifications->send_delete_appointment($appointment_data, $provider_data, - $service_data, $customer_data, $company_settings, $customer_data['email'], + $this->notifications->send_delete_appointment($appointment, $provider, + $service, $customer, $company_settings, $customer['email'], $_POST['delete_reason']); } catch(Exception $exc) { $warnings[] = exceptionToJavascript($exc); @@ -334,7 +345,8 @@ class Backend_api extends CI_Controller { // Add appointment $unavailable = json_decode($_POST['unavailable'], true); - $this->appointments_model->add_unavailable($unavailable); + $unavailable['id'] = $this->appointments_model->add_unavailable($unavailable); + $unavailable = $this->appointments_model->get_row($unavailable['id']); // Google Sync try { @@ -348,11 +360,13 @@ class Backend_api extends CI_Controller { $this->load->library('google_sync'); $this->google_sync->refresh_token($google_token->refresh_token); - // @task Sync with gcal. - $google_event = $this->google_sync->add_unavailable($unavailable); - - $unavailable['id_google_calendar'] = $google_event->id; - $this->appointments_model->add_unavailable($unavailable); + if ($unavailable['id_google_calendar'] == NULL) { + $google_event = $this->google_sync->add_unavailable($unavailable); + $unavailable['id_google_calendar'] = $google_event->id; + $this->appointments_model->add_unavailable($unavailable); + } else { + $google_event = $this->google_sync->update_unavailable($unavailable); + } } } catch(Exception $exc) { $warnings[] = $exc; @@ -380,9 +394,35 @@ class Backend_api extends CI_Controller { */ public function ajax_delete_unavailable() { try { - // Delete unavailable + $this->load->model('appointments_model'); + $this->load->model('providers_model'); + + $unavailable = $this->appointments_model->get_row($_POST['unavailable_id']); + $provider = $this->providers_model->get_row($unavailable['id_users_provider']); + + // Delete unavailable + $this->appointments_model->delete_unavailable($unavailable['id']); // Google Sync + try { + $google_sync = $this->providers_model->get_setting('google_sync', $provider['id']); + 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_unavailable($unavailable['id_google_calendar']); + } + } catch(Exception $exc) { + $warnings[] = $exc; + } + + if (isset($warnings)) { + echo json_encode(array( + 'warnings' => $warnings + )); + } else { + echo json_encode('SUCCESS'); + } } catch(Exception $exc) { echo json_encode(array( diff --git a/src/application/controllers/google.php b/src/application/controllers/google.php index 0a0a5a5c..fcf25975 100644 --- a/src/application/controllers/google.php +++ b/src/application/controllers/google.php @@ -63,6 +63,125 @@ class Google extends CI_Controller { echo '

Authorization Failed!

'; } } + + /** + * Complete synchronization of appointments between Google Calendar and Easy!Appointments. + * + * This method will completely sync the appointments of a provider with his Google Calendar + * account. The sync period needs to be relatively small, because a lot of API calls might + * be necessary and this will lead to consuming the Google limit for the Calendar API usage. + * + * @param numeric $provider_id Provider record to be synced. + * + * @task This method must be executed only by the system and noone else outside. It is a big security issue. + */ + public function sync($provider_id = NULL) { + try { + if ($provider_id === NULL) { + throw new Exception('Provider id not specified.'); + } + + $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'); + + $provider = $this->providers_model->get_row($provider_id); + + // Check whether the selected provider has google sync enabled. + $google_sync = $this->providers_model->get_setting('google_sync', $provider['id']); + if (!$google_sync) { + throw new Exception('The selected provider has not the google synchronization ' + . 'setting enabled.'); + } + + $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); + + // Fetch provider's appointments that belong to the sync time period. + $sync_past_days = $this->providers_model->get_setting('sync_past_days', $provider['id']); + $sync_future_days = $this->providers_model->get_setting('sync_future_days', $provider['id']); + $start = strtotime('-' . $sync_past_days . ' days', strtotime(date('Y-m-d'))); + $end = strtotime('+' . $sync_future_days . ' days', strtotime(date('Y-m-d'))); + + $where_clause = array( + 'start_datetime >=' => date('Y-m-d H:i:s', $start), + 'end_datetime <=' => date('Y-m-d H:i:s', $end), + 'id_users_provider' => $provider['id'], + 'is_unavailable' => FALSE + ); + + $appointments = $this->appointments_model->get_batch($where_clause); + + $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') + ); + + // Sync each appointment with Google Calendar by following the project's sync + // protocol (see documentation). + foreach($appointments as $appointment) { + $service = $this->services_model->get_row($appointment['id_services']); + $customer = $this->customers_model->get_row($appointment['id_users_customer']); + + // :: APPOINTMENT WITH NO GCAL_ID -> ADD TO GCAL + if ($appointment['id_google_calendar'] == NULL) { + $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); // save gcal id + } + + // :: SYNCED APPOINTMENT NOT FOUND ON GCAL -> DELETE E!A RECORD + if ($appointment['id_google_calendar'] != NULL) { + try { + $google_event = $this->google_sync->get_event($appointment['id_google_calendar']); + } catch(Exception $exc) { + $this->appointments_model->delete($appointment['id']); + $appointment['id_google_calendar'] = NULL; // Do not proceed with the rest sync actions. + } + } + + // :: SYNCED APPOINTMENT DIFFERENT FROM GCAL EVENT -> UPDATE E!A RECORD + if ($appointment['id_google_calendar'] != NULL) { + $is_different = FALSE; + $appt_start = strtotime($appointment['start_datetime']); + $appt_end = strtotime($appointment['end_datetime']); + $event_start = strtotime($google_event->getStart()->getDateTime()); + $event_end = strtotime($google_event->getEnd()->getDateTime()); + + if ($appt_start != $event_start + || $appt_end != $event_end) { + $is_different = TRUE; + } + + if ($is_different) { + $appointment['start_datetime'] = date('Y-m-d H:i:s', $event_start); + $appointment['end_datetime'] = date('Y-m-d H:i:s', $event_end); + $this->appointments_model->add($appointment); + } + + } + + // @task :: GCAL EVENT NOT FOUND ON E!A -> ADD EVENT TO E!A + + + } + + // @task Sync unavailable periods with Google Calendar + + + echo json_encode('SUCCESS'); + + } catch(Exception $exc) { + echo json_encode(array( + 'exceptions' => array($exc) + )); + } + } } /* End of file google.php */ diff --git a/src/application/libraries/Unit_tests/drivers/Unit_tests_appointments_model.php b/src/application/libraries/Unit_tests/drivers/Unit_tests_appointments_model.php index 7b502405..083acfc0 100644 --- a/src/application/libraries/Unit_tests/drivers/Unit_tests_appointments_model.php +++ b/src/application/libraries/Unit_tests/drivers/Unit_tests_appointments_model.php @@ -82,6 +82,7 @@ class Unit_tests_appointments_model extends CI_Driver { unset($db_data['hash']); unset($db_data['book_datetime']); unset($db_data['id_google_calendar']); + unset($db_data['is_unavailable']); $this->CI->unit->run($appointment, $db_data, 'Test if add() appointment (insert ' . 'operation) has successfully inserted a record.'); @@ -438,6 +439,7 @@ class Unit_tests_appointments_model extends CI_Driver { $db_data = $this->CI->appointments_model->get_row($appointment['id']); unset($db_data['book_datetime']); unset($db_data['id_google_calendar']); + unset($db_data['is_unavailable']); // Check if this is the record we seek. $this->CI->unit->run($db_data, $appointment, 'Test get_row() method.'); diff --git a/src/application/libraries/google_sync.php b/src/application/libraries/google_sync.php index 337f7a51..b37b7c88 100644 --- a/src/application/libraries/google_sync.php +++ b/src/application/libraries/google_sync.php @@ -160,8 +160,6 @@ class Google_Sync { $event = $this->service->events->get('primary', $appointment['id_google_calendar']); - // Convert event to object - $event->setSummary($service['name']); $event->setLocation($company_settings['company_name']); @@ -203,8 +201,76 @@ class Google_Sync { $this->service->events->delete('primary', $google_calendar_id); } - public function add_unavailble($unavailable) { - // @task Implement + /** + * Add unavailable period event to Google Calendar. + * + * @param array $unavailable Contains unavailable period's data. + * @return Google_Event Returns the google event's object. + */ + public function add_unavailable($unavailable) { + $this->CI->load->helper('general'); + + $event = new Google_Event(); + $event->setSummary('Unavailalbe'); + $event->setDescription($unavailable['notes']); + + $start = new Google_EventDateTime(); + $start->setDateTime(date3339(strtotime($unavailable['start_datetime']))); + $event->setStart($start); + + $end = new Google_EventDateTime(); + $end->setDateTime(date3339(strtotime($unavailable['end_datetime']))); + $event->setEnd($end); + + // Add the new event to the "primary" calendar. + $created_event = $this->service->events->insert('primary', $event); + + return $created_event; + + } + + /** + * Update Google Calendar unavailable period event. + * + * @param array $unavailable Contains the unavailable period data. + * @return Google_Event Returns the Google_Event object. + */ + public function update_unavailable($unavailable) { + $this->CI->load->helper('general'); + + $event = $this->service->events->get('primary', $unavailable['id_google_calendar']); + $event->setDescription($unavailable['notes']); + + $start = new Google_EventDateTime(); + $start->setDateTime(date3339(strtotime($unavailable['start_datetime']))); + $event->setStart($start); + + $end = new Google_EventDateTime(); + $end->setDateTime(date3339(strtotime($unavailable['end_datetime']))); + $event->setEnd($end); + + $updated_event = $this->service->events->update('primary', $event->getId(), $event); + + return $updated_event; + } + + /** + * Delete unavailable period event from Google Calendar. + * + * @param string $google_calendar_id Google Calendar event id to be deleted. + */ + public function delete_unavailable($google_calendar_id) { + $this->service->events->delete('primary', $google_calendar_id); + } + + /** + * Get an event object from gcal + * + * @param string $google_calendar_id Id of the google calendar event + * @return Google_Event Returns the google event object. + */ + public function get_event($google_calendar_id) { + return $this->service->events->get('primary', $google_calendar_id); } } diff --git a/src/application/models/appointments_model.php b/src/application/models/appointments_model.php index b992c4d3..c980011a 100644 --- a/src/application/models/appointments_model.php +++ b/src/application/models/appointments_model.php @@ -360,7 +360,7 @@ class Appointments_Model extends CI_Model { */ public function delete_unavailable($unavailable_id) { if (!is_numeric($unavailable_id)) { - throw new Exception('Invalid argument type $appointment_id (value:"' . + throw new Exception('Invalid argument type $unavailable_id (value:"' . $unavailable_id . '")'); } diff --git a/src/application/models/customers_model.php b/src/application/models/customers_model.php index 4cfd1a95..4e118247 100644 --- a/src/application/models/customers_model.php +++ b/src/application/models/customers_model.php @@ -129,7 +129,7 @@ class Customers_Model extends CI_Model { */ public function find_record_id($customer) { if (!isset($customer['email'])) { - throw new InvalidArgumentException('Customer\'s email was not provided : ' + throw new Exception('Customer\'s email was not provided : ' . print_r($customer, TRUE)); } @@ -244,7 +244,7 @@ class Customers_Model extends CI_Model { } if (!is_string($field_name)) { - throw new IException('$field_name argument is not a string : ' + throw new Exception('$field_name argument is not a string : ' . $field_name); } diff --git a/src/application/views/backend/calendar.php b/src/application/views/backend/calendar.php index 355f3d27..3a827610 100644 --- a/src/application/views/backend/calendar.php +++ b/src/application/views/backend/calendar.php @@ -15,7 +15,8 @@ 'availableProviders' : , 'availableServices' : , 'baseUrl' : , - 'bookAdvanceTimeout' : + 'bookAdvanceTimeout' : , + 'editAppointment' : }; $(document).ready(function() { diff --git a/src/application/views/backend/header.php b/src/application/views/backend/header.php index ad2dc23e..05c3579a 100644 --- a/src/application/views/backend/header.php +++ b/src/application/views/backend/header.php @@ -1,7 +1,7 @@ - Easy!Appointments Backend + Easy!Appointments Backend | <?php echo $company_name; ?> $customer_address - +

Appointment Link

$appointment_link diff --git a/src/assets/css/backend.css b/src/assets/css/backend.css index 9ec97aa8..5550028f 100644 --- a/src/assets/css/backend.css +++ b/src/assets/css/backend.css @@ -51,6 +51,7 @@ root { font-size: 24px; border: none; border-radius: 0; font-weight: bold; color: #333; text-shadow: 0px 1px 0px #FFF;} #calendar-page #calendar .fc-break { background-image: url('../images/break.jpg'); } +#calendar-page #calendar .fc-custom { background-image: url('../images/custom.jpg'); } /* BACKEND CUSTOMERS PAGE -------------------------------------------------------------------- */ diff --git a/src/assets/images/custom.jpg b/src/assets/images/custom.jpg new file mode 100644 index 00000000..e173cb76 Binary files /dev/null and b/src/assets/images/custom.jpg differ diff --git a/src/assets/js/backend_calendar.js b/src/assets/js/backend_calendar.js index 94997aab..7031ea51 100644 --- a/src/assets/js/backend_calendar.js +++ b/src/assets/js/backend_calendar.js @@ -28,6 +28,8 @@ var BackendCalendar = { 'height': BackendCalendar.getCalendarHeight(), 'editable': true, 'slotMinutes': 30, + 'axisFormat': 'HH:mm', + 'timeFormat': 'HH:mm{ - HH:mm}', 'columnFormat': { 'month': 'ddd', 'week': 'ddd d/M', @@ -85,6 +87,11 @@ var BackendCalendar = { BackendCalendar.bindEventHandlers(); $('#select-filter-item').trigger('change'); } + + // :: DISPLAY EDIT DIALOG IF APPOINTMENT HASH IS PROVIDED + if (GlobalVariables.editAppointment != null) { + // @task Display appointment edit dialog to user. + } }, /** @@ -160,38 +167,58 @@ var BackendCalendar = { $(document).on('click', '.edit-popover', function() { $(this).parents().eq(2).remove(); // Hide the popover - var appointment = BackendCalendar.lastFocusedEventData.data; - var $dialog = $('#manage-appointment'); + var $dialog; - BackendCalendar.resetAppointmentDialog(); + if (BackendCalendar.lastFocusedEventData.data.is_unavailable == false) { + var appointment = BackendCalendar.lastFocusedEventData.data; + $dialog = $('#manage-appointment'); + + BackendCalendar.resetAppointmentDialog(); + + // :: APPLY APPOINTMENT DATA AND SHOW TO MODAL DIALOG + $dialog.find('.modal-header h3').text('Edit Appointment'); + $dialog.find('#appointment-id').val(appointment['id']); + $dialog.find('#select-service').val(appointment['id_services']); + $dialog.find('#select-provider').val(appointment['id_users_provider']); + + // Set the start and end datetime of the appointment.\ + var startDatetime = Date.parseExact(appointment['start_datetime'], + 'yyyy-MM-dd HH:mm:ss').toString('dd/MM/yyyy HH:mm'); + $dialog.find('#start-datetime').val(startDatetime); + + var endDatetime = Date.parseExact(appointment['end_datetime'], + 'yyyy-MM-dd HH:mm:ss').toString('dd/MM/yyyy HH:mm'); + $dialog.find('#end-datetime').val(endDatetime); + + var customer = appointment['customer']; + $dialog.find('#customer-id').val(appointment['id_users_customer']); + $dialog.find('#first-name').val(customer['first_name']); + $dialog.find('#last-name').val(customer['last_name']); + $dialog.find('#email').val(customer['email']); + $dialog.find('#phone-number').val(customer['phone_number']); + $dialog.find('#address').val(customer['address']); + $dialog.find('#city').val(customer['city']); + $dialog.find('#zip-code').val(customer['zip_code']); + $dialog.find('#notes').val(appointment['notes']); + } else { + var unavailable = BackendCalendar.lastFocusedEventData.data; + + // Replace string date values with actual date objects. + unavailable.start_datetime = GeneralFunctions.clone(BackendCalendar.lastFocusedEventData.start); + unavailable.end_datetime = GeneralFunctions.clone(BackendCalendar.lastFocusedEventData.end); + + $dialog = $('#manage-unavailable'); + BackendCalendar.resetUnavailableDialog(); + + // :: APPLY UNAVAILABLE DATA TO DIALOG + $dialog.find('.modal-header h3').text('Edit Unavailable Period'); + $dialog.find('#unavailable-id').val(unavailable.id); + $dialog.find('#unavailable-start').val(unavailable.start_datetime.toString('dd/MM/yyyy HH:mm')); + $dialog.find('#unavailable-end').val(unavailable.end_datetime.toString('dd/MM/yyyy HH:mm')); + $dialog.find('#unavailable-notes').val(unavailable.notes); + } - // :: APPLY APPOINTMENT DATA AND SHOW TO MODAL DIALOG - $dialog.find('.modal-header h3').text('Edit Appointment'); - $dialog.find('#appointment-id').val(appointment['id']); - $dialog.find('#select-service').val(appointment['id_services']); - $dialog.find('#select-provider').val(appointment['id_users_provider']); - - // Set the start and end datetime of the appointment.\ - var startDatetime = Date.parseExact(appointment['start_datetime'], - 'yyyy-MM-dd HH:mm:ss').toString('dd/MM/yyyy HH:mm'); - $dialog.find('#start-datetime').val(startDatetime); - - var endDatetime = Date.parseExact(appointment['end_datetime'], - 'yyyy-MM-dd HH:mm:ss').toString('dd/MM/yyyy HH:mm'); - $dialog.find('#end-datetime').val(endDatetime); - - var customer = appointment['customer']; - $dialog.find('#customer-id').val(appointment['id_users_customer']); - $dialog.find('#first-name').val(customer['first_name']); - $dialog.find('#last-name').val(customer['last_name']); - $dialog.find('#email').val(customer['email']); - $dialog.find('#phone-number').val(customer['phone_number']); - $dialog.find('#address').val(customer['address']); - $dialog.find('#city').val(customer['city']); - $dialog.find('#zip-code').val(customer['zip_code']); - $dialog.find('#notes').val(appointment['notes']); - - // :: DISPLAY THE MANAGE APPOINTMENTS MODAL DIALOG + // :: DIAPLY EDIT DIALOG $dialog.modal('show'); }); @@ -205,48 +232,81 @@ var BackendCalendar = { $(document).on('click', '.delete-popover', function() { $(this).parents().eq(2).remove(); // Hide the popover - var messageButtons = { - 'OK': function() { - var postUrl = GlobalVariables.baseUrl + 'backend_api/ajax_delete_appointment'; - - var postData = { - 'appointment_id' : BackendCalendar.lastFocusedEventData.data['id'], - 'delete_reason': $('#delete-reason').val() - }; - - $.post(postUrl, postData, function(response) { - ///////////////////////////////////////////////////////// - console.log('Delete Appointment Response :', response); - ///////////////////////////////////////////////////////// - + if (BackendCalendar.lastFocusedEventData.data.is_unavailable == false) { + var messageButtons = { + 'OK': function() { + var postUrl = GlobalVariables.baseUrl + 'backend_api/ajax_delete_appointment'; + + var postData = { + 'appointment_id' : BackendCalendar.lastFocusedEventData.data['id'], + 'delete_reason': $('#delete-reason').val() + }; + + $.post(postUrl, postData, function(response) { + ///////////////////////////////////////////////////////// + console.log('Delete Appointment Response :', response); + ///////////////////////////////////////////////////////// + + $('#message_box').dialog('close'); + + if (response.exceptions) { + response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); + GeneralFunctions.displayMessageBox(Backend.EXCEPTIONS_TITLE, Backend.EXCEPTIONS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + return; + } + + if (response.warnings) { + response.warnings = GeneralFunctions.parseExceptions(response.warnings); + GeneralFunctions.displayMessageBox(Backend.WARNINGS_TITLE, Backend.WARNINGS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); + } + + // Refresh calendar event items. + $('#select-filter-item').trigger('change'); + }, 'json'); + }, + 'Cancel': function() { $('#message_box').dialog('close'); - - if (response.exceptions) { - response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); - GeneralFunctions.displayMessageBox(Backend.EXCEPTIONS_TITLE, Backend.EXCEPTIONS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); - return; - } - - if (response.warnings) { - response.warnings = GeneralFunctions.parseExceptions(response.warnings); - GeneralFunctions.displayMessageBox(Backend.WARNINGS_TITLE, Backend.WARNINGS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); - } - - // Refresh calendar event items. - $('#select-filter-item').trigger('change'); - }, 'json'); - }, - 'Cancel': function() { + } + }; + + GeneralFunctions.displayMessageBox('Delete Appointment', 'Please take a minute ' + + 'to write the reason you are deleting the appointment:', messageButtons); + $('#message_box').append(''); + $('#delete-reason').css('width', '320px'); + } else { + // Do not display confirmation promt. + var postUrl = GlobalVariables.baseUrl + 'backend_api/ajax_delete_unavailable'; + + var postData = { + 'unavailable_id' : BackendCalendar.lastFocusedEventData.data.id + }; + + $.post(postUrl, postData, function(response) { + ///////////////////////////////////////////////////////// + console.log('Delete Unavailable Response :', response); + ///////////////////////////////////////////////////////// + $('#message_box').dialog('close'); - } - }; - - GeneralFunctions.displayMessageBox('Delete Appointment', 'Please take a minute ' - + 'to write the reason you are deleting the appointment:', messageButtons); - $('#message_box').append(''); - $('#delete-reason').css('width', '320px'); + + if (response.exceptions) { + response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); + GeneralFunctions.displayMessageBox(Backend.EXCEPTIONS_TITLE, Backend.EXCEPTIONS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + return; + } + + if (response.warnings) { + response.warnings = GeneralFunctions.parseExceptions(response.warnings); + GeneralFunctions.displayMessageBox(Backend.WARNINGS_TITLE, Backend.WARNINGS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); + } + + // Refresh calendar event items. + $('#select-filter-item').trigger('change'); + }, 'json'); + } }); /** @@ -353,6 +413,11 @@ var BackendCalendar = { successCallback, errorCallback); }); + /** + * Event: Manage Unavailable Dialog Save Button "Click" + * + * Stores the unavailable period changes or inserts a new record. + */ $('#manage-unavailable #save-unavailable').click(function() { var $dialog = $('#manage-unavailable'); @@ -365,6 +430,7 @@ var BackendCalendar = { '
' + 'Start date value is bigger than end date!' + '
'); + $dialog.find('.modal-message').show(); return; } @@ -376,6 +442,11 @@ var BackendCalendar = { 'id_users_provider': $('#select-filter-item').val() // curr provider }; + if ($dialog.find('#unavailable-id').val() !== '') { + // Set the id value, only if we are editing an appointment. + unavailable.id = $dialog.find('#unavailable-id').val(); + } + var successCallback = function(response) { /////////////////////////////////////////////////////////////////// console.log('Save Unavailable Time Period Response:', response); @@ -385,6 +456,12 @@ var BackendCalendar = { response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); GeneralFunctions.displayMessageBox(Backend.EXCEPTIONS_TITLE, Backend.EXCEPTIONS_MESSAGE); $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + + $dialog.find('.modal-header').append( + '
' + + 'Unexpected issues occured!' + + '
'); + return; } @@ -393,8 +470,20 @@ var BackendCalendar = { GeneralFunctions.displayMessageBox(Backend.WARNINGS_TITLE, Backend.WARNINGS_MESSAGE); $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); } - - $('#manage-unavailable').modal('hide'); + + // Display success message to the user. + $dialog.find('.modal-header').append( + '
' + + 'Appointment saved successfully!' + + '
'); + + // Close the modal dialog and refresh the calendar appointments + // after one second. + setTimeout(function() { + $dialog.find('.alert').remove(); + $dialog.modal('hide'); + $('#select-filter-item').trigger('change'); + }, 2000); }; var errorCallback = function(jqXHR, textStatus, errorThrown) { @@ -404,11 +493,21 @@ var BackendCalendar = { GeneralFunctions.displayMessageBox('Communication Error', 'Unfortunately ' + 'the operation could not complete due to server communication errors.'); + + $dialog.find('.modal-header').append( + '
' + + 'A server communication error occured, please try again.' + + '
'); }; - BackendCalendar.saveUnavalable(unavailable, successCallback, errorCallback); + BackendCalendar.saveUnavailable(unavailable, successCallback, errorCallback); }); + /** + * Event: Manage Unavailable Dialog Cancel Button "Click" + * + * Closes the dialog without saveing any changes to the database. + */ $('#manage-unavailable #cancel-unavailable').click(function() { $('#manage-unavailable').modal('hide'); }); @@ -551,8 +650,9 @@ var BackendCalendar = { // :: ADD APPOINTMENTS TO CALENDAR var calendarEvents = []; + var $calendar = $('#calendar'); - $.each(response, function(index, appointment){ + $.each(response.appointments, function(index, appointment){ var event = { 'id': appointment['id'], 'title': appointment['service']['name'] + ' - ' @@ -571,7 +671,8 @@ var BackendCalendar = { $calendar.fullCalendar('addEventSource', calendarEvents); // :: ADD PROVIDER'S UNAVAILABLE TIME PERIODS - var calendarView = $('#calendar').fullCalendar('getView').name; + var calendarView = $calendar.fullCalendar('getView').name; + if (filterType === BackendCalendar.FILTER_TYPE_PROVIDER && calendarView !== 'month') { @@ -582,11 +683,11 @@ var BackendCalendar = { switch(calendarView) { case 'agendaDay': - var selDayName = $('#calendar').fullCalendar('getView') + var selDayName = $calendar.fullCalendar('getView') .start.toString('dddd').toLowerCase(); // Add unavailable period before work starts. - var calendarDateStart = $('#calendar').fullCalendar('getView').start; + var calendarDateStart = $calendar.fullCalendar('getView').start; var workDateStart = Date.parseExact( calendarDateStart.toString('dd/MM/yyyy') + ' ' + workingPlan[selDayName].start, @@ -602,11 +703,11 @@ var BackendCalendar = { 'editable': false, 'className': 'fc-unavailable' }; - $('#calendar').fullCalendar('renderEvent', unavailablePeriod, true); + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); } // Add unavailable period after work ends. - var calendarDateEnd = $('#calendar').fullCalendar('getView').end; + var calendarDateEnd = $calendar.fullCalendar('getView').end; var workDateEnd = Date.parseExact( calendarDateStart.toString('dd/MM/yyyy') + ' ' + workingPlan[selDayName].end, @@ -621,7 +722,7 @@ var BackendCalendar = { 'editable': false, 'className': 'fc-unavailable' }; - $('#calendar').fullCalendar('renderEvent', unavailablePeriod, true); + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); } // Add unavailable periods for breaks. @@ -640,16 +741,28 @@ var BackendCalendar = { 'editable': false, 'className': 'fc-unavailable fc-break' }; - $('#calendar').fullCalendar('renderEvent', unavailablePeriod, true); + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); }); - // @task Add custom unavailable periods. - + // Add custom unavailable periods. + $.each(response.unavailables, function(index, unavailable) { + var unavailablePeriod = { + 'title': 'Unavailable', + 'start': Date.parse(unavailable.start_datetime), + 'end': Date.parse(unavailable.end_datetime), + 'allDay': false, + 'color': '#123456', + 'editable': true, + 'className': 'fc-unavailable fc-custom', + 'data': unavailable + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + }); break; case 'agendaWeek': - var currDateStart = GeneralFunctions.clone($('#calendar').fullCalendar('getView').start); + var currDateStart = GeneralFunctions.clone($calendar.fullCalendar('getView').start); var currDateEnd = GeneralFunctions.clone(currDateStart).addDays(1); $.each(workingPlan, function(index, workingDay) { @@ -668,7 +781,7 @@ var BackendCalendar = { 'editable': false, 'className': 'fc-unavailable' }; - $('#calendar').fullCalendar('renderEvent', unavailablePeriod, + $calendar.fullCalendar('renderEvent', unavailablePeriod, true); } @@ -685,7 +798,7 @@ var BackendCalendar = { 'editable': false, 'className': 'fc-unavailable fc-brake' }; - $('#calendar').fullCalendar('renderEvent', unavailablePeriod, true); + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); } // Add unavailable periods during day breaks. @@ -704,10 +817,26 @@ var BackendCalendar = { 'editable': false, 'className': 'fc-unavailable fc-break' }; - $('#calendar').fullCalendar('renderEvent', unavailablePeriod, true); + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); }); - // @task Add custom unavailable periods. + // Add custom unavailable periods. + $.each(response.unavailables, function(index, unavailable) { + if (currDateStart.toString('dd/MM/yyyy') + === Date.parse(unavailable.start_datetime).toString('dd/MM/yyyy')) { + var unavailablePeriod = { + 'title': 'Unavailable', + 'start': Date.parse(unavailable.start_datetime), + 'end': Date.parse(unavailable.end_datetime), + 'allDay': false, + 'color': '#123456', + 'editable': true, + 'className': 'fc-unavailable fc-custom', + 'data': unavailable + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + } + }); currDateStart.addDays(1); currDateEnd.addDays(1); @@ -778,7 +907,7 @@ var BackendCalendar = { * @param {function} successCallback The ajax success callback function. * @param {function} errorCallback The ajax failure callback function. */ - saveUnavalable: function(unavailable, successCallback, errorCallback) { + saveUnavailable: function(unavailable, successCallback, errorCallback) { var postUrl = GlobalVariables.baseUrl + 'backend_api/ajax_save_unavailable'; var postData = { @@ -809,64 +938,117 @@ var BackendCalendar = { $('#notification').hide('bind'); } - // :: PREPARE THE APPOINTMENT DATA - var appointment = GeneralFunctions.clone(event.data); + if (event.data.is_unavailable == false) { + // :: PREPARE THE APPOINTMENT DATA + var appointment = GeneralFunctions.clone(event.data); - // Must delete the following because only appointment data should be - // provided to the ajax call. - delete appointment['customer']; - delete appointment['provider']; - delete appointment['service']; + // Must delete the following because only appointment data should be + // provided to the ajax call. + delete appointment['customer']; + delete appointment['provider']; + delete appointment['service']; - appointment['end_datetime'] = Date.parseExact( - appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ minutes: minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); + appointment['end_datetime'] = Date.parseExact( + appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ minutes: minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); - // :: DEFINE THE SUCCESS CALLBACK FUNCTION - var successCallback = function(response) { - if (response.exceptions) { - response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); - GeneralFunctions.displayMessageBox(Backend.EXCEPTIONS_TITLE, Backend.EXCEPTIONS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); - return; - } - - if (response.warnings) { - // Display warning information to the user. - response.warnings = GeneralFunctions.parseExceptions(response.warnings); - GeneralFunctions.displayMessageBox(Backend.WARNINGS_TITLE, Backend.WARNINGS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); - } + // :: DEFINE THE SUCCESS CALLBACK FUNCTION + var successCallback = function(response) { + if (response.exceptions) { + response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); + GeneralFunctions.displayMessageBox(Backend.EXCEPTIONS_TITLE, Backend.EXCEPTIONS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + return; + } - // Display success notification to user. - var undoFunction = function() { - appointment['end_datetime'] = Date.parseExact( - appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ minutes: -minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - var postUrl = GlobalVariables.baseUrl + 'backend_api/ajax_save_appointment'; - var postData = { 'appointment_data': JSON.stringify(appointment) }; + if (response.warnings) { + // Display warning information to the user. + response.warnings = GeneralFunctions.parseExceptions(response.warnings); + GeneralFunctions.displayMessageBox(Backend.WARNINGS_TITLE, Backend.WARNINGS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); + } - $.post(postUrl, postData, function(response) { - $('#notification').hide('blind'); - revertFunc(); - }); + // Display success notification to user. + var undoFunction = function() { + appointment['end_datetime'] = Date.parseExact( + appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ minutes: -minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + var postUrl = GlobalVariables.baseUrl + 'backend_api/ajax_save_appointment'; + var postData = { 'appointment_data': JSON.stringify(appointment) }; + + $.post(postUrl, postData, function(response) { + $('#notification').hide('blind'); + revertFunc(); + }); + }; + + Backend.displayNotification('Appointment updated successfully!', [ + { + 'label': 'Undo', + 'function': undoFunction + } + ]); + $('#footer').css('position', 'static'); // Footer position fix. }; - Backend.displayNotification('Appointment updated successfully!', [ - { - 'label': 'Undo', - 'function': undoFunction - } - ]); - $('#footer').css('position', 'static'); // Footer position fix. - }; + // :: UPDATE APPOINTMENT DATA VIA AJAX CALL + BackendCalendar.saveAppointment(appointment, undefined, + successCallback, undefined); + } else { + // :: UPDATE UNAVAILABLE TIME PERIOD + var unavailable = { + 'id': event.data.id, + 'start_datetime': event.start.toString('yyyy-MM-dd HH:mm:ss'), + 'end_datetime': event.end.toString('yyyy-MM-dd HH:mm:ss'), + 'id_users_provider': event.data.id_users_provider + }; - // :: UPDATE APPOINTMENT DATA VIA AJAX CALL - BackendCalendar.saveAppointment(appointment, undefined, - successCallback, undefined); + // :: DEFINE THE SUCCESS CALLBACK FUNCTION + var successCallback = function(response) { + if (response.exceptions) { + response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); + GeneralFunctions.displayMessageBox(Backend.EXCEPTIONS_TITLE, Backend.EXCEPTIONS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + return; + } + + if (response.warnings) { + // Display warning information to the user. + response.warnings = GeneralFunctions.parseExceptions(response.warnings); + GeneralFunctions.displayMessageBox(Backend.WARNINGS_TITLE, Backend.WARNINGS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); + } + + // Display success notification to user. + var undoFunction = function() { + unavailable['end_datetime'] = Date.parseExact( + unavailable['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ minutes: -minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + var postUrl = GlobalVariables.baseUrl + 'backend_api/ajax_save_unavailable'; + var postData = { 'unavailable': JSON.stringify(unavailable) }; + + $.post(postUrl, postData, function(response) { + $('#notification').hide('blind'); + revertFunc(); + }); + }; + + Backend.displayNotification('Unavailable time period updated successfully!', [ + { + 'label': 'Undo', + 'function': undoFunction + } + ]); + $('#footer').css('position', 'static'); // Footer position fix. + }; + + BackendCalendar.saveUnavailable(unavailable, successCallback, undefined); + } }, /** @@ -905,10 +1087,23 @@ var BackendCalendar = { calendarEventClick: function(event, jsEvent, view) { $('.popover').remove(); // Close all open popovers. - var html; + var html; // Popover's html code - if ($(jsEvent.target.offsetParent).hasClass('fc-unavailable') - || $(jsEvent.target).parents().eq(1).hasClass('fc-unavailable')) { + // Depending where the user clicked the event (title or empty space) we + // need to use different selectors to reach the parent element. + var $parent = $(jsEvent.target.offsetParent); + var $altParent = $(jsEvent.target).parents().eq(1); + + if ($parent.hasClass('fc-unavailable') || $altParent.hasClass('fc-unavailable')) { + var displayEdit = ($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) + ? '' : 'hide'; + var displayDelete = displayEdit; // Same value at the time. + + var notes = ''; + if (event.data) { // Only custom unavailable periods have notes. + notes = 'Notes ' + event.data.notes; + } + html = '