From 84558912623ac821f0cf23cd32051fde62830ae9 Mon Sep 17 00:00:00 2001 From: "alextselegidis@gmail.com" Date: Wed, 10 Jul 2013 13:57:24 +0000 Subject: [PATCH] * Finished with unavailable time periods management (backend). * Started google sync operation (complete sync). * Minor changes on js files. --- src/application/controllers/appointments.php | 73 +- src/application/controllers/backend.php | 45 +- src/application/controllers/backend_api.php | 108 ++- src/application/controllers/google.php | 119 +++ .../drivers/Unit_tests_appointments_model.php | 2 + src/application/libraries/google_sync.php | 74 +- src/application/models/appointments_model.php | 2 +- src/application/models/customers_model.php | 4 +- src/application/views/backend/calendar.php | 3 +- src/application/views/backend/header.php | 2 +- .../views/emails/appointment_details.php | 2 +- src/assets/css/backend.css | 1 + src/assets/images/custom.jpg | Bin 0 -> 160205 bytes src/assets/js/backend_calendar.js | 705 ++++++++++++------ src/assets/js/frontend_book.js | 2 +- 15 files changed, 818 insertions(+), 324 deletions(-) create mode 100644 src/assets/images/custom.jpg 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 0000000000000000000000000000000000000000..e173cb7617429531bc43b4c8b24499bb373c6692 GIT binary patch literal 160205 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'); + $('#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 = '