diff --git a/application/controllers/Admins.php b/application/controllers/Admins.php new file mode 100644 index 00000000..6c6ffd01 --- /dev/null +++ b/application/controllers/Admins.php @@ -0,0 +1,173 @@ + + * @copyright Copyright (c) 2013 - 2020, Alex Tselegidis + * @license https://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link https://easyappointments.org + * @since v1.0.0 + * ---------------------------------------------------------------------------- */ + +/** + * Admins controller. + * + * Handles the admins related operations. + * + * @package Controllers + */ +class Admins extends EA_Controller { + /** + * Admins constructor. + */ + public function __construct() + { + parent::__construct(); + + $this->load->model('admins_model'); + $this->load->model('roles_model'); + + $this->load->library('accounts'); + $this->load->library('timezones'); + } + + /** + * Render the backend admins page. + * + * On this page admin users will be able to manage admins, which are eventually selected by customers during the + * booking process. + */ + public function index() + { + session(['dest_url' => site_url('admins')]); + + if (cannot('view', 'users')) + { + show_error('Forbidden', 403); + } + + $user_id = session('user_id'); + + $role_slug = session('role_slug'); + + $this->load->view('pages/admins/admins_page', [ + 'page_title' => lang('admins'), + 'active_menu' => PRIV_USERS, + 'user_display_name' => $this->accounts->get_user_display_name($user_id), + 'timezones' => $this->timezones->to_array(), + 'privileges' => $this->roles_model->get_permissions_by_slug($role_slug), + ]); + } + + /** + * Filter admins by the provided keyword. + */ + public function search() + { + try + { + if (cannot('view', 'users')) + { + show_error('Forbidden', 403); + } + + $keyword = request('keyword', ''); + + $order_by = 'first_name ASC, last_name ASC, email ASC'; + + $limit = request('limit', 1000); + + $offset = 0; + + $admins = $this->admins_model->search($keyword, $limit, $offset, $order_by); + + json_response($admins); + } + catch (Throwable $e) + { + json_exception($e); + } + } + + /** + * Create a admin. + */ + public function create() + { + try + { + $admin = json_decode(request('admin'), TRUE); + + if (cannot('add', 'users')) + { + show_error('Forbidden', 403); + } + + $admin_id = $this->admins_model->save($admin); + + json_response([ + 'success' => TRUE, + 'id' => $admin_id + ]); + } + catch (Throwable $e) + { + json_exception($e); + } + } + + /** + * Update a admin. + */ + public function update() + { + try + { + $admin = json_decode(request('admin'), TRUE); + + if (cannot('edit', 'users')) + { + show_error('Forbidden', 403); + } + + $admin_id = $this->admins_model->save($admin); + + json_response([ + 'success' => TRUE, + 'id' => $admin_id + ]); + } + catch (Throwable $e) + { + json_exception($e); + } + } + + /** + * Remove a admin. + */ + public function destroy() + { + try + { + if (cannot('delete', 'users')) + { + show_error('Forbidden', 403); + } + + $admin_id = request('admin_id'); + + $this->admins_model->delete($admin_id); + + json_response([ + 'success' => TRUE, + ]); + } + catch (Throwable $e) + { + json_exception($e); + } + } +} diff --git a/application/views/pages/admins/admins_page.php b/application/views/pages/admins/admins_page.php new file mode 100755 index 00000000..e47ab957 --- /dev/null +++ b/application/views/pages/admins/admins_page.php @@ -0,0 +1,237 @@ + + + + + + + + + + +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ +

+ +
+
+ +
+
+
+ + + +
+ + +
+ +

+ + + + + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+
+
+
+ + + diff --git a/assets/js/backend_admins.js b/assets/js/backend_admins.js new file mode 100644 index 00000000..fb1c4100 --- /dev/null +++ b/assets/js/backend_admins.js @@ -0,0 +1,107 @@ +/* ---------------------------------------------------------------------------- + * Easy!Appointments - Open Source Web Scheduler + * + * @package EasyAppointments + * @author A.Tselegidis + * @copyright Copyright (c) 2013 - 2020, Alex Tselegidis + * @license http://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link http://easyappointments.org + * @since v1.0.0 + * ---------------------------------------------------------------------------- */ + +window.BackendAdmins = window.BackendAdmins || {}; + +/** + * Backend Admins + * + * This module handles the js functionality of the admins backend page. It uses three other + * classes (defined below) in order to handle the admin, admin and secretary record types. + * + * @module BackendAdmins + */ +(function (exports) { + 'use strict'; + + /** + * Minimum Password Length + * + * @type {Number} + */ + exports.MIN_PASSWORD_LENGTH = 7; + + /** + * Contains the current tab record methods for the page. + * + * @type {AdminsHelper} + */ + var helper = {}; + + /** + * Initialize the backend admins page. + * + * @param {Boolean} defaultEventHandlers (OPTIONAL) Whether to bind the default event handlers. + */ + exports.initialize = function (defaultEventHandlers) { + defaultEventHandlers = defaultEventHandlers || true; + + // Instantiate default helper object (admin). + helper = new AdminsHelper(); + helper.resetForm(); + helper.filter(''); + helper.bindEventHandlers(); + + // Bind event handlers. + if (defaultEventHandlers) { + bindEventHandlers(); + } + }; + + /** + * Binds the default backend admins event handlers. Do not use this method on a different + * page because it needs the backend admins page DOM. + */ + function bindEventHandlers() { + /** + * Event: Admin Username "Blur" + * + * When the admin leaves the username input field we will need to check if the username + * is not taken by another record in the system. + */ + $('#admin-username').focusout(function () { + var $input = $(this); + + if ($input.prop('readonly') === true || $input.val() === '') { + return; + } + + var adminId = $input.parents().eq(2).find('.record-id').val(); + + if (!adminId) { + return; + } + + var url = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_validate_username'; + + var data = { + csrfToken: GlobalVariables.csrfToken, + username: $input.val(), + user_id: adminId + }; + + $.post(url, data).done(function (response) { + if (response.is_valid === 'false') { + $input.closest('.form-group').addClass('has-error'); + $input.attr('already-exists', 'true'); + $input.parents().eq(3).find('.form-message').text(EALang.username_already_exists); + $input.parents().eq(3).find('.form-message').show(); + } else { + $input.closest('.form-group').removeClass('has-error'); + $input.attr('already-exists', 'false'); + if ($input.parents().eq(3).find('.form-message').text() === EALang.username_already_exists) { + $input.parents().eq(3).find('.form-message').hide(); + } + } + }); + }); + } +})(window.BackendAdmins); diff --git a/assets/js/backend_admins_helper.js b/assets/js/backend_admins_helper.js new file mode 100644 index 00000000..762437c4 --- /dev/null +++ b/assets/js/backend_admins_helper.js @@ -0,0 +1,488 @@ +/* ---------------------------------------------------------------------------- + * Easy!Appointments - Open Source Web Scheduler + * + * @package EasyAppointments + * @author A.Tselegidis + * @copyright Copyright (c) 2013 - 2020, Alex Tselegidis + * @license http://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link http://easyappointments.org + * @since v1.0.0 + * ---------------------------------------------------------------------------- */ + +(function () { + 'use strict'; + + /** + * This class contains the Admins helper class declaration, along with the "Admins" tab + * event handlers. By dividing the backend/users tab functionality into separate files + * it is easier to maintain the code. + * + * @class AdminsHelper + */ + var AdminsHelper = function () { + this.filterResults = []; // Store the results for later use. + this.filterLimit = 20; + }; + + /** + * Bind the event handlers for the backend/users "Admins" tab. + */ + AdminsHelper.prototype.bindEventHandlers = function () { + /** + * Event: Filter Admins Form "Submit" + * + * Filter the admin records with the given key string. + * + * @param {jQuery.Event} event + */ + $('#admins').on( + 'submit', + '#filter-admins form', + function (event) { + event.preventDefault(); + var key = $('#filter-admins .key').val(); + $('#filter-admins .selected').removeClass('selected'); + this.resetForm(); + this.filter(key); + }.bind(this) + ); + + /** + * Event: Clear Filter Results Button "Click" + */ + $('#admins').on( + 'click', + '#filter-admins .clear', + function () { + this.filter(''); + $('#filter-admins .key').val(''); + this.resetForm(); + }.bind(this) + ); + + /** + * Event: Filter Admin Row "Click" + * + * Display the selected admin data to the user. + */ + $('#admins').on( + 'click', + '.admin-row', + function (event) { + if ($('#filter-admins .filter').prop('disabled')) { + $('#filter-admins .results').css('color', '#AAA'); + return; // exit because we are currently on edit mode + } + + var adminId = $(event.currentTarget).attr('data-id'); + + var admin = this.filterResults.find(function (filterResult) { + return Number(filterResult.id) === Number(adminId); + }); + + this.display(admin); + $('#filter-admins .selected').removeClass('selected'); + $(event.currentTarget).addClass('selected'); + $('#edit-admin, #delete-admin').prop('disabled', false); + }.bind(this) + ); + + /** + * Event: Add New Admin Button "Click" + */ + $('#admins').on( + 'click', + '#add-admin', + function () { + this.resetForm(); + $('#admins .add-edit-delete-group').hide(); + $('#admins .save-cancel-group').show(); + $('#admins .record-details').find('input, textarea').prop('disabled', false); + $('#admins .record-details').find('select').prop('disabled', false); + $('#admin-password, #admin-password-confirm').addClass('required'); + $('#filter-admins button').prop('disabled', true); + $('#filter-admins .results').css('color', '#AAA'); + }.bind(this) + ); + + /** + * Event: Edit Admin Button "Click" + */ + $('#admins').on('click', '#edit-admin', function () { + $('#admins .add-edit-delete-group').hide(); + $('#admins .save-cancel-group').show(); + $('#admins .record-details').find('input, textarea').prop('disabled', false); + $('#admins .record-details').find('select').prop('disabled', false); + $('#admin-password, #admin-password-confirm').removeClass('required'); + $('#filter-admins button').prop('disabled', true); + $('#filter-admins .results').css('color', '#AAA'); + }); + + /** + * Event: Delete Admin Button "Click" + */ + $('#admins').on( + 'click', + '#delete-admin', + function () { + var adminId = $('#admin-id').val(); + + var buttons = [ + { + text: EALang.cancel, + click: function () { + $('#message-box').dialog('close'); + } + }, + { + text: EALang.delete, + click: function () { + this.delete(adminId); + $('#message-box').dialog('close'); + }.bind(this) + } + ]; + + GeneralFunctions.displayMessageBox(EALang.delete_admin, EALang.delete_record_prompt, buttons); + }.bind(this) + ); + + /** + * Event: Save Admin Button "Click" + */ + $('#admins').on( + 'click', + '#save-admin', + function () { + var admin = { + first_name: $('#admin-first-name').val(), + last_name: $('#admin-last-name').val(), + email: $('#admin-email').val(), + mobile_number: $('#admin-mobile-number').val(), + phone_number: $('#admin-phone-number').val(), + address: $('#admin-address').val(), + city: $('#admin-city').val(), + state: $('#admin-state').val(), + zip_code: $('#admin-zip-code').val(), + notes: $('#admin-notes').val(), + timezone: $('#admin-timezone').val(), + settings: { + username: $('#admin-username').val(), + notifications: $('#admin-notifications').prop('checked'), + calendar_view: $('#admin-calendar-view').val() + } + }; + + // Include password if changed. + if ($('#admin-password').val() !== '') { + admin.settings.password = $('#admin-password').val(); + } + + // Include id if changed. + if ($('#admin-id').val() !== '') { + admin.id = $('#admin-id').val(); + } + + if (!this.validate()) { + return; + } + + this.save(admin); + }.bind(this) + ); + + /** + * Event: Cancel Admin Button "Click" + * + * Cancel add or edit of an admin record. + */ + $('#admins').on( + 'click', + '#cancel-admin', + function () { + var id = $('#admin-id').val(); + this.resetForm(); + if (id) { + this.select(id, true); + } + }.bind(this) + ); + }; + + /** + * Remove the previously registered event handlers. + */ + AdminsHelper.prototype.unbindEventHandlers = function () { + $('#admins') + .off('submit', '#filter-admins form') + .off('click', '#filter-admins .clear') + .off('click', '.admin-row') + .off('click', '#add-admin') + .off('click', '#edit-admin') + .off('click', '#delete-admin') + .off('click', '#save-admin') + .off('click', '#cancel-admin'); + }; + + /** + * Save admin record to database. + * + * @param {Object} admin Contains the admin record data. If an 'id' value is provided + * then the update operation is going to be executed. + */ + AdminsHelper.prototype.save = function (admin) { + var url = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_admin'; + + var data = { + csrfToken: GlobalVariables.csrfToken, + admin: JSON.stringify(admin) + }; + + $.post(url, data).done( + function (response) { + Backend.displayNotification(EALang.admin_saved); + this.resetForm(); + $('#filter-admins .key').val(''); + this.filter('', response.id, true); + }.bind(this) + ); + }; + + /** + * Delete an admin record from database. + * + * @param {Number} id Record id to be deleted. + */ + AdminsHelper.prototype.delete = function (id) { + var url = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_delete_admin'; + + var data = { + csrfToken: GlobalVariables.csrfToken, + admin_id: id + }; + + $.post(url, data).done( + function (response) { + Backend.displayNotification(EALang.admin_deleted); + this.resetForm(); + this.filter($('#filter-admins .key').val()); + }.bind(this) + ); + }; + + /** + * Validates an admin record. + * + * @return {Boolean} Returns the validation result. + */ + AdminsHelper.prototype.validate = function () { + $('#admins .has-error').removeClass('has-error'); + + try { + // Validate required fields. + var missingRequired = false; + + $('#admins .required').each(function (index, requiredField) { + if (!$(requiredField).val()) { + $(requiredField).closest('.form-group').addClass('has-error'); + missingRequired = true; + } + }); + + if (missingRequired) { + throw new Error('Fields with * are required.'); + } + + // Validate passwords. + if ($('#admin-password').val() !== $('#admin-password-confirm').val()) { + $('#admin-password, #admin-password-confirm').closest('.form-group').addClass('has-error'); + throw new Error(EALang.passwords_mismatch); + } + + if ( + $('#admin-password').val().length < BackendAdmins.MIN_PASSWORD_LENGTH && + $('#admin-password').val() !== '' + ) { + $('#admin-password, #admin-password-confirm').closest('.form-group').addClass('has-error'); + throw new Error(EALang.password_length_notice.replace('$number', BackendAdmins.MIN_PASSWORD_LENGTH)); + } + + // Validate user email. + if (!GeneralFunctions.validateEmail($('#admin-email').val())) { + $('#admin-email').closest('.form-group').addClass('has-error'); + throw new Error(EALang.invalid_email); + } + + // Check if username exists + if ($('#admin-username').attr('already-exists') === 'true') { + $('#admin-username').closest('.form-group').addClass('has-error'); + throw new Error(EALang.username_already_exists); + } + + return true; + } catch (error) { + $('#admins .form-message').addClass('alert-danger').text(error.message).show(); + return false; + } + }; + + /** + * Resets the admin form back to its initial state. + */ + AdminsHelper.prototype.resetForm = function () { + $('#filter-admins .selected').removeClass('selected'); + $('#filter-admins button').prop('disabled', false); + $('#filter-admins .results').css('color', ''); + + $('#admins .add-edit-delete-group').show(); + $('#admins .save-cancel-group').hide(); + $('#admins .record-details').find('input, select, textarea').val('').prop('disabled', true); + $('#admins .record-details #admin-calendar-view').val('default'); + $('#admins .record-details #admin-timezone').val('UTC'); + $('#edit-admin, #delete-admin').prop('disabled', true); + + $('#admins .has-error').removeClass('has-error'); + $('#admins .form-message').hide(); + }; + + /** + * Display a admin record into the admin form. + * + * @param {Object} admin Contains the admin record data. + */ + AdminsHelper.prototype.display = function (admin) { + $('#admin-id').val(admin.id); + $('#admin-first-name').val(admin.first_name); + $('#admin-last-name').val(admin.last_name); + $('#admin-email').val(admin.email); + $('#admin-mobile-number').val(admin.mobile_number); + $('#admin-phone-number').val(admin.phone_number); + $('#admin-address').val(admin.address); + $('#admin-city').val(admin.city); + $('#admin-state').val(admin.state); + $('#admin-zip-code').val(admin.zip_code); + $('#admin-notes').val(admin.notes); + $('#admin-timezone').val(admin.timezone); + + $('#admin-username').val(admin.settings.username); + $('#admin-calendar-view').val(admin.settings.calendar_view); + $('#admin-notifications').prop('checked', Boolean(Number(admin.settings.notifications))); + }; + + /** + * Filters admin records depending a keyword string. + * + * @param {String} keyword This string is used to filter the admin records of the database. + * @param {Number} selectId (OPTIONAL = undefined) This record id will be selected when + * the filter operation is finished. + * @param {Boolean} display (OPTIONAL = false) If true the selected record data are going + * to be displayed on the details column (requires a selected record though). + */ + AdminsHelper.prototype.filter = function (keyword, selectId, display) { + display = display || false; + + var url = GlobalVariables.baseUrl + '/index.php/admins/search'; + + var data = { + csrfToken: GlobalVariables.csrfToken, + keyword: keyword, + limit: this.filterLimit + }; + + $.post(url, data).done( + function (response) { + this.filterResults = response; + + $('#filter-admins .results').empty(); + + response.forEach( + function (admin) { + $('#filter-admins .results').append(this.getFilterHtml(admin)).append($('
')); + }.bind(this) + ); + + if (!response.length) { + $('#filter-admins .results').append( + $('', { + 'text': EALang.no_records_found + }) + ); + } else if (response.length === this.filterLimit) { + $('