* @copyright Copyright (c) Alex Tselegidis * @license https://opensource.org/licenses/GPL-3.0 - GPLv3 * @link https://easyappointments.org * @since v1.5.0 * ---------------------------------------------------------------------------- */ /** * Calendar controller. * * Handles calendar related operations. * * @package Controllers */ class Calendar extends EA_Controller { public array $allowed_customer_fields = [ 'id', 'first_name', 'last_name', 'email', 'phone_number', 'address', 'city', 'state', 'zip_code', 'timezone', 'language', 'notes', 'custom_field_1', 'custom_field_2', 'custom_field_3', 'custom_field_4', 'custom_field_5', ]; public array $allowed_appointment_fields = [ 'id', 'start_datetime', 'end_datetime', 'location', 'notes', 'color', 'status', 'is_unavailability', 'id_users_provider', 'id_users_customer', 'id_services', ]; /** * Calendar constructor. */ public function __construct() { parent::__construct(); $this->load->model('appointments_model'); $this->load->model('unavailabilities_model'); $this->load->model('blocked_periods_model'); $this->load->model('customers_model'); $this->load->model('services_model'); $this->load->model('providers_model'); $this->load->model('roles_model'); $this->load->library('accounts'); $this->load->library('google_sync'); $this->load->library('notifications'); $this->load->library('synchronization'); $this->load->library('timezones'); $this->load->library('webhooks_client'); } /** * Render the calendar page and display the selected appointment. * * This method will call the "index" callback to handle the page rendering. * * @param string $appointment_hash Appointment hash. */ public function reschedule(string $appointment_hash): void { $this->index($appointment_hash); } /** * Display the main backend page. * * This method displays the main backend page. All login permission can view this page which displays a calendar * with the events of the selected provider or service. If a user has more privileges he will see more menus at the * top of the page. * * @param string $appointment_hash Appointment hash. */ public function index(string $appointment_hash = ''): void { session(['dest_url' => site_url('backend/index' . (!empty($appointment_hash) ? '/' . $appointment_hash : ''))]); $user_id = session('user_id'); if (cannot('view', PRIV_APPOINTMENTS)) { if ($user_id) { abort(403, 'Forbidden'); } redirect('login'); return; } $role_slug = session('role_slug'); $user = $this->users_model->find($user_id); $secretary_providers = []; if ($role_slug === DB_SLUG_SECRETARY) { $secretary = $this->secretaries_model->find(session('user_id')); $secretary_providers = $secretary['providers']; } $edit_appointment = null; if (!empty($appointment_hash)) { $occurrences = $this->appointments_model->get(['hash' => $appointment_hash]); if ($appointment_hash !== '' && !empty($occurrences)) { $edit_appointment = $occurrences[0]; $this->appointments_model->load($edit_appointment, ['customer']); } } $privileges = $this->roles_model->get_permissions_by_slug($role_slug); $available_providers = $this->providers_model->get_available_providers(); if ($role_slug === DB_SLUG_PROVIDER) { $available_providers = array_values( array_filter($available_providers, function ($available_provider) use ($user_id) { return (int) $available_provider['id'] === (int) $user_id; }), ); } if ($role_slug === DB_SLUG_SECRETARY) { $available_providers = array_values( array_filter($available_providers, function ($available_provider) use ($secretary_providers) { return in_array($available_provider['id'], $secretary_providers); }), ); } $available_services = $this->services_model->get_available_services(); $calendar_view = request('view', $user['settings']['calendar_view']); $appointment_status_options = setting('appointment_status_options'); script_vars([ 'user_id' => $user_id, 'role_slug' => $role_slug, 'date_format' => setting('date_format'), 'time_format' => setting('time_format'), 'first_weekday' => setting('first_weekday'), 'company_working_plan' => setting('company_working_plan'), 'timezones' => $this->timezones->to_array(), 'privileges' => $privileges, 'calendar_view' => $calendar_view, 'available_providers' => $available_providers, 'available_services' => $available_services, 'secretary_providers' => $secretary_providers, 'edit_appointment' => $edit_appointment, 'customers' => $this->customers_model->get(null, 50, null, 'update_datetime DESC'), ]); html_vars([ 'page_title' => lang('calendar'), 'active_menu' => PRIV_APPOINTMENTS, 'user_display_name' => $this->accounts->get_user_display_name($user_id), 'timezone' => session('timezone'), 'timezones' => $this->timezones->to_array(), 'grouped_timezones' => $this->timezones->to_grouped_array(), 'privileges' => $privileges, 'calendar_view' => $calendar_view, 'available_providers' => $available_providers, 'available_services' => $available_services, 'secretary_providers' => $secretary_providers, 'appointment_status_options' => json_decode($appointment_status_options, true) ?? [], 'require_first_name' => setting('require_first_name'), 'require_last_name' => setting('require_last_name'), 'require_email' => setting('require_email'), 'require_phone_number' => setting('require_phone_number'), 'require_address' => setting('require_address'), 'require_city' => setting('require_city'), 'require_zip_code' => setting('require_zip_code'), 'require_notes' => setting('require_notes'), ]); $this->load->view('pages/calendar'); } /** * Save appointment changes that are made from the backend calendar page. */ public function save_appointment(): void { try { $customer_data = request('customer_data'); $appointment_data = request('appointment_data'); $this->check_event_permissions((int) $appointment_data['id_users_provider']); // Save customer changes to the database. if ($customer_data) { $customer = $customer_data; $required_permissions = !empty($customer['id']) ? can('add', PRIV_CUSTOMERS) : can('edit', PRIV_CUSTOMERS); if (!$required_permissions) { throw new RuntimeException('You do not have the required permissions for this task.'); } $this->customers_model->only($customer, $this->allowed_customer_fields); $customer['id'] = $this->customers_model->save($customer); } // Save appointment changes to the database. $manage_mode = !empty($appointment_data['id']); if ($appointment_data) { $appointment = $appointment_data; $required_permissions = !empty($appointment['id']) ? can('add', PRIV_APPOINTMENTS) : can('edit', PRIV_APPOINTMENTS); if (!$required_permissions) { throw new RuntimeException('You do not have the required permissions for this task.'); } // If the appointment does not contain the customer record id, then it means that is going to be // inserted. if (!isset($appointment['id_users_customer'])) { $appointment['id_users_customer'] = $customer['id'] ?? $customer_data['id']; } if ($manage_mode && !empty($appointment['id'])) { $this->synchronization->remove_appointment_on_provider_change($appointment['id']); } $this->appointments_model->only($appointment, $this->allowed_appointment_fields); $appointment['id'] = $this->appointments_model->save($appointment); } if (empty($appointment['id'])) { throw new RuntimeException('The appointment ID is not available.'); } $appointment = $this->appointments_model->find($appointment['id']); $provider = $this->providers_model->find($appointment['id_users_provider']); $customer = $this->customers_model->find($appointment['id_users_customer']); $service = $this->services_model->find($appointment['id_services']); $settings = [ 'company_name' => setting('company_name'), 'company_link' => setting('company_link'), 'company_email' => setting('company_email'), 'date_format' => setting('date_format'), 'time_format' => setting('time_format'), ]; $this->synchronization->sync_appointment_saved($appointment, $service, $provider, $customer, $settings); $this->notifications->notify_appointment_saved( $appointment, $service, $provider, $customer, $settings, $manage_mode, ); $this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_SAVE, $appointment); json_response([ 'success' => true, ]); } catch (Throwable $e) { json_exception($e); } } private function check_event_permissions(int $provider_id): void { $user_id = (int) session('user_id'); $role_slug = session('role_slug'); if ( $role_slug === DB_SLUG_SECRETARY && !$this->secretaries_model->is_provider_supported($user_id, $provider_id) ) { abort(403); } if ($role_slug === DB_SLUG_PROVIDER && $user_id !== $provider_id) { abort(403); } } /** * Delete appointment from the database. * * This method deletes an existing appointment from the database. Once this action is finished it cannot be undone. * Notification emails are send to both provider and customer and the delete action is executed to the Google * Calendar account of the provider, if the "google_sync" setting is enabled. */ public function delete_appointment(): void { try { if (cannot('delete', 'appointments')) { throw new RuntimeException('You do not have the required permissions for this task.'); } $appointment_id = request('appointment_id'); $cancellation_reason = (string) request('cancellation_reason'); if (empty($appointment_id)) { throw new InvalidArgumentException('No appointment id provided.'); } // Store appointment data for later use in this method. $appointment = $this->appointments_model->find($appointment_id); $this->check_event_permissions((int) $appointment['id_users_provider']); $provider = $this->providers_model->find($appointment['id_users_provider']); $customer = $this->customers_model->find($appointment['id_users_customer']); $service = $this->services_model->find($appointment['id_services']); $settings = [ 'company_name' => setting('company_name'), 'company_email' => setting('company_email'), 'company_link' => setting('company_link'), 'date_format' => setting('date_format'), 'time_format' => setting('time_format'), ]; // Delete appointment record from the database. $this->appointments_model->delete($appointment_id); $this->notifications->notify_appointment_deleted( $appointment, $service, $provider, $customer, $settings, $cancellation_reason, ); $this->synchronization->sync_appointment_deleted($appointment, $provider); $this->webhooks_client->trigger(WEBHOOK_APPOINTMENT_DELETE, $appointment); json_response([ 'success' => true, ]); } catch (Throwable $e) { json_exception($e); } } /** * Insert of update unavailability to database. */ public function save_unavailability(): void { try { // Check privileges $unavailability = request('unavailability'); $required_permissions = !isset($unavailability['id']) ? can('add', PRIV_APPOINTMENTS) : can('edit', PRIV_APPOINTMENTS); if (!$required_permissions) { throw new RuntimeException('You do not have the required permissions for this task.'); } $provider_id = (int) $unavailability['id_users_provider']; $this->check_event_permissions($provider_id); $provider = $this->providers_model->find($provider_id); $unavailability_id = $this->unavailabilities_model->save($unavailability); $unavailability = $this->unavailabilities_model->find($unavailability_id); $this->synchronization->sync_unavailability_saved($unavailability, $provider); $this->webhooks_client->trigger(WEBHOOK_UNAVAILABILITY_SAVE, $unavailability); json_response([ 'success' => true, 'warnings' => $warnings ?? [], ]); } catch (Throwable $e) { json_exception($e); } } /** * Delete an unavailability from database. */ public function delete_unavailability(): void { try { if (cannot('delete', PRIV_APPOINTMENTS)) { throw new RuntimeException('You do not have the required permissions for this task.'); } $unavailability_id = request('unavailability_id'); $unavailability = $this->unavailabilities_model->find($unavailability_id); $this->check_event_permissions((int) $unavailability['id_users_provider']); $provider = $this->providers_model->find($unavailability['id_users_provider']); $this->unavailabilities_model->delete($unavailability_id); $this->synchronization->sync_unavailability_deleted($unavailability, $provider); $this->webhooks_client->trigger(WEBHOOK_UNAVAILABILITY_DELETE, $unavailability); json_response([ 'success' => true, ]); } catch (Throwable $e) { json_exception($e); } } /** * Insert of update working plan exceptions to database. */ public function save_working_plan_exception(): void { try { if (cannot('edit', PRIV_USERS)) { throw new RuntimeException('You do not have the required permissions for this task.'); } $date = request('date'); $working_plan_exception = request('working_plan_exception'); if (!$working_plan_exception) { $working_plan_exception = null; } $provider_id = request('provider_id'); $this->providers_model->save_working_plan_exception($provider_id, $date, $working_plan_exception); json_response([ 'success' => true, ]); } catch (Throwable $e) { json_exception($e); } } /** * Delete a working plan exceptions time period to database. */ public function delete_working_plan_exception(): void { try { $required_permissions = can('edit', PRIV_CUSTOMERS); if (!$required_permissions) { throw new RuntimeException('You do not have the required permissions for this task.'); } $date = request('date'); $provider_id = request('provider_id'); $this->providers_model->delete_working_plan_exception($provider_id, $date); json_response([ 'success' => true, ]); } catch (Throwable $e) { json_exception($e); } } /** * Get Calendar Events * * This method will return all the calendar events within a specified period. */ public function get_calendar_appointments_for_table_view(): void { try { $required_permissions = can('view', PRIV_APPOINTMENTS); if (!$required_permissions) { throw new RuntimeException('You do not have the required permissions for this task.'); } $start_date = request('start_date') . ' 00:00:00'; $end_date = request('end_date') . ' 23:59:59'; $response = [ 'appointments' => $this->appointments_model->get([ 'start_datetime >=' => $start_date, 'end_datetime <=' => $end_date, ]), 'unavailabilities' => $this->unavailabilities_model->get([ 'start_datetime >=' => $start_date, 'end_datetime <=' => $end_date, ]), ]; foreach ($response['appointments'] as &$appointment) { $appointment['provider'] = $this->providers_model->find($appointment['id_users_provider']); $appointment['service'] = $this->services_model->find($appointment['id_services']); $appointment['customer'] = $this->customers_model->find($appointment['id_users_customer']); } unset($appointment); $user_id = session('user_id'); $role_slug = session('role_slug'); // If the current user is a provider he must only see his own appointments. if ($role_slug === DB_SLUG_PROVIDER) { foreach ($response['appointments'] as $index => $appointment) { if ((int) $appointment['id_users_provider'] !== (int) $user_id) { unset($response['appointments'][$index]); } } $response['appointments'] = array_values($response['appointments']); foreach ($response['unavailabilities'] as $index => $unavailability) { if ((int) $unavailability['id_users_provider'] !== (int) $user_id) { unset($response['unavailabilities'][$index]); } } $response['unavailabilities'] = array_values($response['unavailabilities']); } // If the current user is a secretary he must only see the appointments of his providers. if ($role_slug === DB_SLUG_SECRETARY) { $providers = $this->secretaries_model->find($user_id)['providers']; foreach ($response['appointments'] as $index => $appointment) { if (!in_array((int) $appointment['id_users_provider'], $providers)) { unset($response['appointments'][$index]); } } $response['appointments'] = array_values($response['appointments']); foreach ($response['unavailabilities'] as $index => $unavailability) { if (!in_array((int) $unavailability['id_users_provider'], $providers)) { unset($response['unavailabilities'][$index]); } } $response['unavailabilities'] = array_values($response['unavailabilities']); } foreach ($response['unavailabilities'] as &$unavailability) { $unavailability['provider'] = $this->providers_model->find($unavailability['id_users_provider']); } unset($unavailability); // Add blocked periods to the response. $start_date = request('start_date'); $end_date = request('end_date'); $response['blocked_periods'] = $this->blocked_periods_model->get_for_period($start_date, $end_date); json_response($response); } catch (Throwable $e) { json_exception($e); } } /** * Get the registered appointments for the given date period and record. * * This method returns the database appointments and unavailability periods for the user selected date period and * record type (provider or service). */ public function get_calendar_appointments(): void { try { if (cannot('view', PRIV_APPOINTMENTS)) { throw new RuntimeException('You do not have the required permissions for this task.'); } $record_id = request('record_id'); $is_all = request('record_id') === FILTER_TYPE_ALL; $filter_type = request('filter_type'); if (!$filter_type && !$is_all) { json_response([ 'appointments' => [], 'unavailabilities' => [], ]); return; } $record_id = $this->db->escape($record_id); if ($filter_type == FILTER_TYPE_PROVIDER) { $where_id = 'id_users_provider'; } elseif ($filter_type === FILTER_TYPE_SERVICE) { $where_id = 'id_services'; } else { $where_id = $record_id; } // Get appointments $start_date = $this->db->escape(request('start_date')); $end_date = $this->db->escape(date('Y-m-d', strtotime(request('end_date') . ' +1 day'))); $where_clause = $where_id . ' = ' . $record_id . ' AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ') or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ') or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . ')) AND is_unavailability = 0 '; $response['appointments'] = $this->appointments_model->get($where_clause); foreach ($response['appointments'] as &$appointment) { $appointment['provider'] = $this->providers_model->find($appointment['id_users_provider']); $appointment['service'] = $this->services_model->find($appointment['id_services']); $appointment['customer'] = $this->customers_model->find($appointment['id_users_customer']); } unset($appointment); // Get unavailability periods (only for provider). $response['unavailabilities'] = []; if ($filter_type == FILTER_TYPE_PROVIDER || $is_all) { $where_clause = $where_id . ' = ' . $record_id . ' AND ((start_datetime > ' . $start_date . ' AND start_datetime < ' . $end_date . ') or (end_datetime > ' . $start_date . ' AND end_datetime < ' . $end_date . ') or (start_datetime <= ' . $start_date . ' AND end_datetime >= ' . $end_date . ')) AND is_unavailability = 1 '; $response['unavailabilities'] = $this->unavailabilities_model->get($where_clause); } foreach ($response['unavailabilities'] as &$unavailability) { $unavailability['provider'] = $this->providers_model->find($unavailability['id_users_provider']); } unset($appointment); $user_id = session('user_id'); $role_slug = session('role_slug'); // If the current user is a provider he must only see his own appointments. if ($role_slug === DB_SLUG_PROVIDER) { foreach ($response['appointments'] as $index => $appointment) { if ((int) $appointment['id_users_provider'] !== (int) $user_id) { unset($response['appointments'][$index]); } } $response['appointments'] = array_values($response['appointments']); foreach ($response['unavailabilities'] as $index => $unavailability) { if ((int) $unavailability['id_users_provider'] !== (int) $user_id) { unset($response['unavailabilities'][$index]); } } unset($unavailability); $response['unavailabilities'] = array_values($response['unavailabilities']); } // If the current user is a secretary he must only see the appointments of his providers. if ($role_slug === DB_SLUG_SECRETARY) { $providers = $this->secretaries_model->find($user_id)['providers']; foreach ($response['appointments'] as $index => $appointment) { if (!in_array((int) $appointment['id_users_provider'], $providers)) { unset($response['appointments'][$index]); } } $response['appointments'] = array_values($response['appointments']); foreach ($response['unavailabilities'] as $index => $unavailability) { if (!in_array((int) $unavailability['id_users_provider'], $providers)) { unset($response['unavailabilities'][$index]); } } $response['unavailabilities'] = array_values($response['unavailabilities']); } // Add blocked periods to the response. $start_date = request('start_date'); $end_date = request('end_date'); $response['blocked_periods'] = $this->blocked_periods_model->get_for_period($start_date, $end_date); json_response($response); } catch (Throwable $e) { json_exception($e); } } }