first commit
Some checks failed
CI / build-test (push) Has been cancelled

This commit is contained in:
2025-05-31 18:56:37 +02:00
commit 8c4798a5fd
1240 changed files with 190468 additions and 0 deletions

View File

@@ -0,0 +1,665 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Admins model.
*
* Handles all the database operations of the admin resource.
*
* @package Models
*/
class Admins_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'id_roles' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'firstName' => 'first_name',
'lastName' => 'last_name',
'email' => 'email',
'mobile' => 'mobile_number',
'phone' => 'phone_number',
'address' => 'address',
'city' => 'city',
'state' => 'state',
'zip' => 'zip_code',
'timezone' => 'timezone',
'language' => 'language',
'notes' => 'notes',
'ldapDn' => 'ldap_dn',
'roleId' => 'id_roles',
];
/**
* Save (insert or update) an admin.
*
* @param array $admin Associative array with the admin data.
*
* @return int Returns the admin ID.
*
* @throws InvalidArgumentException
* @throws Exception
*/
public function save(array $admin): int
{
$this->validate($admin);
if (empty($admin['id'])) {
return $this->insert($admin);
} else {
return $this->update($admin);
}
}
/**
* Validate the admin data.
*
* @param array $admin Associative array with the admin data.
*
* @throws InvalidArgumentException
*/
public function validate(array $admin): void
{
// If an admin ID is provided then check whether the record really exists in the database.
if (!empty($admin['id'])) {
$count = $this->db->get_where('users', ['id' => $admin['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided admin ID does not exist in the database: ' . $admin['id'],
);
}
}
// Make sure all required fields are provided.
if (
empty($admin['first_name']) ||
empty($admin['last_name']) ||
empty($admin['email']) ||
empty($admin['phone_number'])
) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($admin, true));
}
// Validate the email address.
if (!filter_var($admin['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address provided: ' . $admin['email']);
}
// Make sure the username is unique.
if (!empty($admin['settings']['username'])) {
$admin_id = $admin['id'] ?? null;
if (!$this->validate_username($admin['settings']['username'], $admin_id)) {
throw new InvalidArgumentException(
'The provided username is already in use, please use a different one.',
);
}
}
// Validate the password.
if (!empty($admin['settings']['password'])) {
if (strlen($admin['settings']['password']) < MIN_PASSWORD_LENGTH) {
throw new InvalidArgumentException(
'The admin password must be at least ' . MIN_PASSWORD_LENGTH . ' characters long.',
);
}
}
// New users must always have a password value set.
if (empty($admin['id']) && empty($admin['settings']['password'])) {
throw new InvalidArgumentException('The admin password cannot be empty when inserting a new record.');
}
// Validate calendar view type value.
if (
!empty($admin['settings']['calendar_view']) &&
!in_array($admin['settings']['calendar_view'], [CALENDAR_VIEW_DEFAULT, CALENDAR_VIEW_TABLE])
) {
throw new InvalidArgumentException(
'The provided calendar view is invalid: ' . $admin['settings']['calendar_view'],
);
}
// Make sure the email address is unique.
$admin_id = $admin['id'] ?? null;
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('roles.slug', DB_SLUG_ADMIN)
->where('users.email', $admin['email'])
->where('users.id !=', $admin_id)
->get()
->num_rows();
if ($count > 0) {
throw new InvalidArgumentException(
'The provided email address is already in use, please use a different one.',
);
}
}
/**
* Validate the admin username.
*
* @param string $username Admin username.
* @param int|null $admin_id Admin ID.
*
* @return bool Returns the validation result.
*/
public function validate_username(string $username, ?int $admin_id = null): bool
{
if (!empty($admin_id)) {
$this->db->where('id_users !=', $admin_id);
}
return $this->db
->from('users')
->join('user_settings', 'user_settings.id_users = users.id', 'inner')
->where(['username' => $username])
->get()
->num_rows() === 0;
}
/**
* Get all admins that match the provided criteria.
*
* @param array|string|null $where Where conditions.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of admins.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
$role_id = $this->get_admin_role_id();
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$admins = $this->db->get_where('users', ['id_roles' => $role_id], $limit, $offset)->result_array();
foreach ($admins as &$admin) {
$this->cast($admin);
$admin['settings'] = $this->get_settings($admin['id']);
}
return $admins;
}
/**
* Get the admin role ID.
*
* @return int Returns the role ID.
*/
public function get_admin_role_id(): int
{
$role = $this->db->get_where('roles', ['slug' => DB_SLUG_ADMIN])->row_array();
if (empty($role)) {
throw new RuntimeException('The admin role was not found in the database.');
}
return $role['id'];
}
/**
* Insert a new admin into the database.
*
* @param array $admin Associative array with the admin data.
*
* @return int Returns the admin ID.
*
* @throws RuntimeException|Exception
*/
protected function insert(array $admin): int
{
$admin['id_roles'] = $this->get_admin_role_id();
$settings = $admin['settings'];
unset($admin['settings']);
$admin['create_datetime'] = date('Y-m-d H:i:s');
$admin['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->insert('users', $admin)) {
throw new RuntimeException('Could not insert admin.');
}
$admin['id'] = $this->db->insert_id();
$settings['salt'] = generate_salt();
$settings['password'] = hash_password($settings['salt'], $settings['password']);
$this->set_settings($admin['id'], $settings);
return $admin['id'];
}
/**
* Save the admin settings.
*
* @param int $admin_id Admin ID.
* @param array $settings Associative array with the settings data.
*
* @throws InvalidArgumentException
*/
public function set_settings(int $admin_id, array $settings): void
{
if (empty($settings)) {
throw new InvalidArgumentException('The settings argument cannot be empty.');
}
// Make sure the settings record exists in the database.
$count = $this->db->get_where('user_settings', ['id_users' => $admin_id])->num_rows();
if (!$count) {
$this->db->insert('user_settings', ['id_users' => $admin_id]);
}
foreach ($settings as $name => $value) {
$this->set_setting($admin_id, $name, $value);
}
}
/**
* Get the admin settings.
*
* @param int $admin_id Admin ID.
*
* @throws InvalidArgumentException
*/
public function get_settings(int $admin_id): array
{
$settings = $this->db->get_where('user_settings', ['id_users' => $admin_id])->row_array();
unset($settings['id_users'], $settings['password'], $settings['salt']);
return $settings;
}
/**
* Set the value of an admin setting.
*
* @param int $admin_id Admin ID.
* @param string $name Setting name.
* @param mixed|null $value Setting value.
*/
public function set_setting(int $admin_id, string $name, mixed $value = null): void
{
if (!$this->db->update('user_settings', [$name => $value], ['id_users' => $admin_id])) {
throw new RuntimeException('Could not set the new admin setting value: ' . $name);
}
}
/**
* Update an existing admin.
*
* @param array $admin Associative array with the admin data.
*
* @return int Returns the admin ID.
*
* @throws RuntimeException
* @throws Exception
*/
protected function update(array $admin): int
{
$settings = $admin['settings'];
$settings['id_users'] = $admin['id'];
unset($admin['settings']);
if (!empty($settings['password'])) {
$existing_settings = $this->db->get_where('user_settings', ['id_users' => $admin['id']])->row_array();
if (empty($existing_settings)) {
throw new RuntimeException('No settings record found for admin with ID: ' . $admin['id']);
}
if (empty($existing_settings['salt'])) {
$existing_settings['salt'] = $settings['salt'] = generate_salt();
}
$settings['password'] = hash_password($existing_settings['salt'], $settings['password']);
}
$admin['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('users', $admin, ['id' => $admin['id']])) {
throw new RuntimeException('Could not update admin.');
}
$this->set_settings($admin['id'], $settings);
return $admin['id'];
}
/**
* Remove an existing admin from the database.
*
* @param int $admin_id Admin ID.
*
* @throws RuntimeException
*/
public function delete(int $admin_id): void
{
$role_id = $this->get_admin_role_id();
$count = $this->db->get_where('users', ['id_roles' => $role_id])->num_rows();
if ($count <= 1) {
throw new RuntimeException('Record could not be deleted as the app requires at least one admin user.');
}
$this->db->delete('users', ['id' => $admin_id]);
}
/**
* Get a specific admin from the database.
*
* @param int $admin_id The ID of the record to be returned.
*
* @return array Returns an array with the admin data.
*
* @throws InvalidArgumentException
*/
public function find(int $admin_id): array
{
$admin = $this->db->get_where('users', ['id' => $admin_id])->row_array();
if (!$admin) {
throw new InvalidArgumentException('The provided admin ID was not found in the database: ' . $admin_id);
}
$this->cast($admin);
$admin['settings'] = $this->get_settings($admin['id']);
return $admin;
}
/**
* Get a specific field value from the database.
*
* @param int $admin_id Admin ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected admin value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $admin_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($admin_id)) {
throw new InvalidArgumentException('The admin ID argument cannot be empty.');
}
// Check whether the admin exists.
$query = $this->db->get_where('users', ['id' => $admin_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException('The provided admin ID was not found in the database: ' . $admin_id);
}
// Check if the required field is part of the admin data.
$admin = $query->row_array();
$this->cast($admin);
if (!array_key_exists($field, $admin)) {
throw new InvalidArgumentException('The requested field was not found in the admin data: ' . $field);
}
return $admin[$field];
}
/**
* Get the value of an admin setting.
*
* @param int $admin_id Admin ID.
* @param string $name Setting name.
*
* @return string Returns the value of the requested user setting.
*/
public function get_setting(int $admin_id, string $name): string
{
$settings = $this->db->get_where('user_settings', ['id_users' => $admin_id])->row_array();
if (!array_key_exists($name, $settings)) {
throw new RuntimeException('The requested setting value was not found: ' . $admin_id);
}
return $settings[$name];
}
/**
* Get the query builder interface, configured for use with the users (admin-filtered) table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
$role_id = $this->get_admin_role_id();
return $this->db->from('users')->where('id_roles', $role_id);
}
/**
* Search admins by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of admins.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$role_id = $this->get_admin_role_id();
$admins = $this->db
->select()
->from('users')
->where('id_roles', $role_id)
->group_start()
->like('first_name', $keyword)
->or_like('last_name', $keyword)
->or_like('CONCAT_WS(" ", first_name, last_name)', $keyword)
->or_like('email', $keyword)
->or_like('phone_number', $keyword)
->or_like('mobile_number', $keyword)
->or_like('address', $keyword)
->or_like('city', $keyword)
->or_like('state', $keyword)
->or_like('zip_code', $keyword)
->or_like('notes', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($admins as &$admin) {
$this->cast($admin);
$admin['settings'] = $this->get_settings($admin['id']);
}
return $admins;
}
/**
* Load related resources to an admin.
*
* @param array $admin Associative array with the admin data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$admin, array $resources)
{
// Admins do not currently have any related resources (settings are already part of the admins).
}
/**
* Convert the database admin record to the equivalent API resource.
*
* @param array $admin Admin data.
*/
public function api_encode(array &$admin): void
{
$encoded_resource = [
'id' => array_key_exists('id', $admin) ? (int) $admin['id'] : null,
'firstName' => $admin['first_name'],
'lastName' => $admin['last_name'],
'email' => $admin['email'],
'mobile' => $admin['mobile_number'],
'phone' => $admin['phone_number'],
'address' => $admin['address'],
'city' => $admin['city'],
'state' => $admin['state'],
'zip' => $admin['zip_code'],
'notes' => $admin['notes'],
'timezone' => $admin['timezone'],
'language' => $admin['language'],
'ldapDn' => $admin['ldap_dn'],
'settings' => [
'username' => $admin['settings']['username'],
'notifications' => filter_var($admin['settings']['notifications'], FILTER_VALIDATE_BOOLEAN),
'calendarView' => $admin['settings']['calendar_view'],
],
];
$admin = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database admin record.
*
* @param array $admin API resource.
* @param array|null $base Base admin data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$admin, ?array $base = null): void
{
$decoded_resource = $base ?? [];
if (array_key_exists('id', $admin)) {
$decoded_resource['id'] = $admin['id'];
}
if (array_key_exists('firstName', $admin)) {
$decoded_resource['first_name'] = $admin['firstName'];
}
if (array_key_exists('lastName', $admin)) {
$decoded_resource['last_name'] = $admin['lastName'];
}
if (array_key_exists('email', $admin)) {
$decoded_resource['email'] = $admin['email'];
}
if (array_key_exists('mobile', $admin)) {
$decoded_resource['mobile_number'] = $admin['mobile'];
}
if (array_key_exists('phone', $admin)) {
$decoded_resource['phone_number'] = $admin['phone'];
}
if (array_key_exists('address', $admin)) {
$decoded_resource['address'] = $admin['address'];
}
if (array_key_exists('city', $admin)) {
$decoded_resource['city'] = $admin['city'];
}
if (array_key_exists('state', $admin)) {
$decoded_resource['state'] = $admin['state'];
}
if (array_key_exists('zip', $admin)) {
$decoded_resource['zip_code'] = $admin['zip'];
}
if (array_key_exists('notes', $admin)) {
$decoded_resource['notes'] = $admin['notes'];
}
if (array_key_exists('timezone', $admin)) {
$decoded_resource['timezone'] = $admin['timezone'];
}
if (array_key_exists('language', $admin)) {
$decoded_resource['language'] = $admin['language'];
}
if (array_key_exists('ldapDn', $admin)) {
$decoded_resource['ldap_dn'] = $admin['ldapDn'];
}
if (array_key_exists('settings', $admin)) {
if (empty($decoded_resource['settings'])) {
$decoded_resource['settings'] = [];
}
if (array_key_exists('username', $admin['settings'])) {
$decoded_resource['settings']['username'] = $admin['settings']['username'];
}
if (array_key_exists('password', $admin['settings'])) {
$decoded_resource['settings']['password'] = $admin['settings']['password'];
}
if (array_key_exists('notifications', $admin['settings'])) {
$decoded_resource['settings']['notifications'] = filter_var(
$admin['settings']['notifications'],
FILTER_VALIDATE_BOOLEAN,
);
}
if (array_key_exists('calendarView', $admin['settings'])) {
$decoded_resource['settings']['calendar_view'] = $admin['settings']['calendarView'];
}
}
$admin = $decoded_resource;
}
}

View File

@@ -0,0 +1,648 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Appointments model.
*
* @package Models
*/
class Appointments_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'is_unavailability' => 'boolean',
'id_users_provider' => 'integer',
'id_users_customer' => 'integer',
'id_services' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'book' => 'book_datetime',
'start' => 'start_datetime',
'end' => 'end_datetime',
'location' => 'location',
'color' => 'color',
'status' => 'status',
'notes' => 'notes',
'hash' => 'hash',
'serviceId' => 'id_services',
'providerId' => 'id_users_provider',
'customerId' => 'id_users_customer',
'googleCalendarId' => 'id_google_calendar',
'caldavCalendarId' => 'id_caldav_calendar',
];
/**
* Save (insert or update) an appointment.
*
* @param array $appointment Associative array with the appointment data.
*
* @return int Returns the appointment ID.
*
* @throws InvalidArgumentException
*/
public function save(array $appointment): int
{
$this->validate($appointment);
if (empty($appointment['id'])) {
return $this->insert($appointment);
} else {
return $this->update($appointment);
}
}
/**
* Validate the appointment data.
*
* @param array $appointment Associative array with the appointment data.
*
* @throws InvalidArgumentException
*/
public function validate(array $appointment): void
{
// If an appointment ID is provided then check whether the record really exists in the database.
if (!empty($appointment['id'])) {
$count = $this->db->get_where('appointments', ['id' => $appointment['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided appointment ID does not exist in the database: ' . $appointment['id'],
);
}
}
// Make sure all required fields are provided.
$require_notes = filter_var(setting('require_notes'), FILTER_VALIDATE_BOOLEAN);
if (
empty($appointment['start_datetime']) ||
empty($appointment['end_datetime']) ||
empty($appointment['id_services']) ||
empty($appointment['id_users_provider']) ||
empty($appointment['id_users_customer']) ||
(empty($appointment['notes']) && $require_notes)
) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($appointment, true));
}
// Make sure that the provided appointment date time values are valid.
if (!validate_datetime($appointment['start_datetime'])) {
throw new InvalidArgumentException('The appointment start date time is invalid.');
}
if (!validate_datetime($appointment['end_datetime'])) {
throw new InvalidArgumentException('The appointment end date time is invalid.');
}
// Make the appointment lasts longer than the minimum duration (in minutes).
$diff = (strtotime($appointment['end_datetime']) - strtotime($appointment['start_datetime'])) / 60;
if ($diff < EVENT_MINIMUM_DURATION) {
throw new InvalidArgumentException(
'The appointment duration cannot be less than ' . EVENT_MINIMUM_DURATION . ' minutes.',
);
}
// Make sure the provider ID really exists in the database.
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('users.id', $appointment['id_users_provider'])
->where('roles.slug', DB_SLUG_PROVIDER)
->get()
->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The appointment provider ID was not found in the database: ' . $appointment['id_users_provider'],
);
}
if (!filter_var($appointment['is_unavailability'], FILTER_VALIDATE_BOOLEAN)) {
// Make sure the customer ID really exists in the database.
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('users.id', $appointment['id_users_customer'])
->where('roles.slug', DB_SLUG_CUSTOMER)
->get()
->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The appointment customer ID was not found in the database: ' . $appointment['id_users_customer'],
);
}
// Make sure the service ID really exists in the database.
$count = $this->db->get_where('services', ['id' => $appointment['id_services']])->num_rows();
if (!$count) {
throw new InvalidArgumentException('Appointment service id is invalid.');
}
}
}
/**
* Get all appointments that match the provided criteria.
*
* @param array|string|null $where Where conditions.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of appointments.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by) {
$this->db->order_by($order_by);
}
$appointments = $this->db
->get_where('appointments', ['is_unavailability' => false], $limit, $offset)
->result_array();
foreach ($appointments as &$appointment) {
$this->cast($appointment);
}
return $appointments;
}
/**
* Insert a new appointment into the database.
*
* @param array $appointment Associative array with the appointment data.
*
* @return int Returns the appointment ID.
*
* @throws RuntimeException
*/
protected function insert(array $appointment): int
{
$appointment['book_datetime'] = date('Y-m-d H:i:s');
$appointment['create_datetime'] = date('Y-m-d H:i:s');
$appointment['update_datetime'] = date('Y-m-d H:i:s');
$appointment['hash'] = random_string('alnum', 12);
if (!$this->db->insert('appointments', $appointment)) {
throw new RuntimeException('Could not insert appointment.');
}
return $this->db->insert_id();
}
/**
* Update an existing appointment.
*
* @param array $appointment Associative array with the appointment data.
*
* @return int Returns the appointment ID.
*
* @throws RuntimeException
*/
protected function update(array $appointment): int
{
$appointment['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('appointments', $appointment, ['id' => $appointment['id']])) {
throw new RuntimeException('Could not update appointment record.');
}
return $appointment['id'];
}
/**
* Remove an existing appointment from the database.
*
* @param int $appointment_id Appointment ID.
*
* @throws RuntimeException
*/
public function delete(int $appointment_id): void
{
$this->db->delete('appointments', ['id' => $appointment_id]);
}
/**
* Get a specific appointment from the database.
*
* @param int $appointment_id The ID of the record to be returned.
*
* @return array Returns an array with the appointment data.
*
* @throws InvalidArgumentException
*/
public function find(int $appointment_id): array
{
$appointment = $this->db->get_where('appointments', ['id' => $appointment_id])->row_array();
if (!$appointment) {
throw new InvalidArgumentException(
'The provided appointment ID was not found in the database: ' . $appointment_id,
);
}
$this->cast($appointment);
return $appointment;
}
/**
* Get a specific field value from the database.
*
* @param int $appointment_id Appointment ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected appointment value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $appointment_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($appointment_id)) {
throw new InvalidArgumentException('The appointment ID argument cannot be empty.');
}
// Check whether the appointment exists.
$query = $this->db->get_where('appointments', ['id' => $appointment_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException(
'The provided appointment ID was not found in the database: ' . $appointment_id,
);
}
// Check if the required field is part of the appointment data.
$appointment = $query->row_array();
$this->cast($appointment);
if (!array_key_exists($field, $appointment)) {
throw new InvalidArgumentException('The requested field was not found in the appointment data: ' . $field);
}
return $appointment[$field];
}
/**
* Remove all the Google Calendar event IDs from appointment records.
*
* @param int $provider_id Matching provider ID.
*/
public function clear_google_sync_ids(int $provider_id): void
{
$this->db->update('appointments', ['id_google_calendar' => null], ['id_users_provider' => $provider_id]);
}
/**
* Remove all the Google Calendar event IDs from appointment records.
*
* @param int $provider_id Matching provider ID.
*/
public function clear_caldav_sync_ids(int $provider_id): void
{
$this->db->update('appointments', ['id_caldav_calendar' => null], ['id_users_provider' => $provider_id]);
}
/**
* Deletes recurring CalDAV events for the provided date period.
*
* @param string $start_date_time
* @param string $end_date_time
*
* @return void
*/
public function delete_caldav_recurring_events(string $start_date_time, string $end_date_time): void
{
$this->db
->where('start_datetime >=', $start_date_time)
->where('end_datetime <=', $end_date_time)
->like('id_caldav_calendar', '%RECURRENCE%')
->delete('appointments');
}
/**
* Get the attendants number for the requested period.
*
* @param DateTime $start Period start.
* @param DateTime $end Period end.
* @param int $service_id Service ID.
* @param int $provider_id Provider ID.
* @param int|null $exclude_appointment_id Exclude an appointment from the result set.
*
* @return int Returns the number of appointments that match the provided criteria.
*/
public function get_attendants_number_for_period(
DateTime $start,
DateTime $end,
int $service_id,
int $provider_id,
?int $exclude_appointment_id = null,
): int {
if ($exclude_appointment_id) {
$this->db->where('id !=', $exclude_appointment_id);
}
$result = $this->db
->select('count(*) AS attendants_number')
->from('appointments')
->group_start()
->group_start()
->where('start_datetime <=', $start->format('Y-m-d H:i:s'))
->where('end_datetime >', $start->format('Y-m-d H:i:s'))
->group_end()
->or_group_start()
->where('start_datetime <', $end->format('Y-m-d H:i:s'))
->where('end_datetime >=', $end->format('Y-m-d H:i:s'))
->group_end()
->group_end()
->where('id_services', $service_id)
->where('id_users_provider', $provider_id)
->get()
->row_array();
return $result['attendants_number'];
}
/**
*
* Returns the number of the other service attendants number for the provided time slot.
*
* @param DateTime $start Period start.
* @param DateTime $end Period end.
* @param int $service_id Service ID.
* @param int $provider_id Provider ID.
* @param int|null $exclude_appointment_id Exclude an appointment from the result set.
*
* @return int Returns the number of appointments that match the provided criteria.
*/
public function get_other_service_attendants_number(
DateTime $start,
DateTime $end,
int $service_id,
int $provider_id,
?int $exclude_appointment_id = null,
): int {
if ($exclude_appointment_id) {
$this->db->where('id !=', $exclude_appointment_id);
}
$result = $this->db
->select('count(*) AS attendants_number')
->from('appointments')
->group_start()
->group_start()
->where('start_datetime <=', $start->format('Y-m-d H:i:s'))
->where('end_datetime >', $start->format('Y-m-d H:i:s'))
->group_end()
->or_group_start()
->where('start_datetime <', $end->format('Y-m-d H:i:s'))
->where('end_datetime >=', $end->format('Y-m-d H:i:s'))
->group_end()
->group_end()
->where('id_services !=', $service_id)
->where('id_users_provider', $provider_id)
->get()
->row_array();
return $result['attendants_number'];
}
/**
* Get the query builder interface, configured for use with the appointments table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('appointments');
}
/**
* Search appointments by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of appointments.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$appointments = $this->db
->select('appointments.*')
->from('appointments')
->join('services', 'services.id = appointments.id_services', 'left')
->join('users AS providers', 'providers.id = appointments.id_users_provider', 'inner')
->join('users AS customers', 'customers.id = appointments.id_users_customer', 'left')
->where('is_unavailability', false)
->group_start()
->like('appointments.start_datetime', $keyword)
->or_like('appointments.end_datetime', $keyword)
->or_like('appointments.location', $keyword)
->or_like('appointments.hash', $keyword)
->or_like('appointments.notes', $keyword)
->or_like('services.name', $keyword)
->or_like('services.description', $keyword)
->or_like('providers.first_name', $keyword)
->or_like('providers.last_name', $keyword)
->or_like('providers.email', $keyword)
->or_like('providers.phone_number', $keyword)
->or_like('customers.first_name', $keyword)
->or_like('customers.last_name', $keyword)
->or_like('customers.email', $keyword)
->or_like('customers.phone_number', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($appointments as &$appointment) {
$this->cast($appointment);
}
return $appointments;
}
/**
* Load related resources to an appointment.
*
* @param array $appointment Associative array with the appointment data.
* @param array $resources Resource names to be attached ("service", "provider", "customer" supported).
*
* @throws InvalidArgumentException
*/
public function load(array &$appointment, array $resources): void
{
if (empty($appointment) || empty($resources)) {
return;
}
foreach ($resources as $resource) {
switch ($resource) {
case 'service':
$appointment['service'] = $this->db
->get_where('services', [
'id' => $appointment['id_services'] ?? ($appointment['serviceId'] ?? null),
])
->row_array();
break;
case 'provider':
$appointment['provider'] = $this->db
->get_where('users', [
'id' => $appointment['id_users_provider'] ?? ($appointment['providerId'] ?? null),
])
->row_array();
break;
case 'customer':
$appointment['customer'] = $this->db
->get_where('users', [
'id' => $appointment['id_users_customer'] ?? ($appointment['customerId'] ?? null),
])
->row_array();
break;
default:
throw new InvalidArgumentException(
'The requested appointment relation is not supported: ' . $resource,
);
}
}
}
/**
* Convert the database appointment record to the equivalent API resource.
*
* @param array $appointment Appointment data.
*/
public function api_encode(array &$appointment): void
{
$encoded_resource = [
'id' => array_key_exists('id', $appointment) ? (int) $appointment['id'] : null,
'book' => $appointment['book_datetime'],
'start' => $appointment['start_datetime'],
'end' => $appointment['end_datetime'],
'hash' => $appointment['hash'],
'color' => $appointment['color'],
'status' => $appointment['status'],
'location' => $appointment['location'],
'notes' => $appointment['notes'],
'customerId' => $appointment['id_users_customer'] !== null ? (int) $appointment['id_users_customer'] : null,
'providerId' => $appointment['id_users_provider'] !== null ? (int) $appointment['id_users_provider'] : null,
'serviceId' => $appointment['id_services'] !== null ? (int) $appointment['id_services'] : null,
'googleCalendarId' =>
$appointment['id_google_calendar'] !== null ? $appointment['id_google_calendar'] : null,
'caldavCalendarId' =>
$appointment['id_caldav_calendar'] !== null ? $appointment['id_caldav_calendar'] : null,
];
$appointment = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database appointment record.
*
* @param array $appointment API resource.
* @param array|null $base Base appointment data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$appointment, ?array $base = null): void
{
$decoded_request = $base ?: [];
if (array_key_exists('id', $appointment)) {
$decoded_request['id'] = $appointment['id'];
}
if (array_key_exists('book', $appointment)) {
$decoded_request['book_datetime'] = $appointment['book'];
}
if (array_key_exists('start', $appointment)) {
$decoded_request['start_datetime'] = $appointment['start'];
}
if (array_key_exists('end', $appointment)) {
$decoded_request['end_datetime'] = $appointment['end'];
}
if (array_key_exists('hash', $appointment)) {
$decoded_request['hash'] = $appointment['hash'];
}
if (array_key_exists('location', $appointment)) {
$decoded_request['location'] = $appointment['location'];
}
if (array_key_exists('status', $appointment)) {
$decoded_request['status'] = $appointment['status'];
}
if (array_key_exists('notes', $appointment)) {
$decoded_request['notes'] = $appointment['notes'];
}
if (array_key_exists('customerId', $appointment)) {
$decoded_request['id_users_customer'] = $appointment['customerId'];
}
if (array_key_exists('providerId', $appointment)) {
$decoded_request['id_users_provider'] = $appointment['providerId'];
}
if (array_key_exists('serviceId', $appointment)) {
$decoded_request['id_services'] = $appointment['serviceId'];
}
if (array_key_exists('googleCalendarId', $appointment)) {
$decoded_request['id_google_calendar'] = $appointment['googleCalendarId'];
}
if (array_key_exists('caldavCalendarId', $appointment)) {
$decoded_request['id_caldav_calendar'] = $appointment['caldavCalendarId'];
}
$decoded_request['is_unavailability'] = false;
$appointment = $decoded_request;
}
}

View File

@@ -0,0 +1,413 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Blocked-Periods model.
*
* Handles all the database operations of the blocked-period resource.
*
* @package Models
*/
class Blocked_periods_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'name' => 'name',
'start' => 'start_datetime',
'end' => 'end_datetime',
'notes' => 'notes',
];
/**
* Save (insert or update) a blocked-period.
*
* @param array $blocked_period Associative array with the blocked-period data.
*
* @return int Returns the blocked-period ID.
*
* @throws InvalidArgumentException
* @throws Exception
*/
public function save(array $blocked_period): int
{
$this->validate($blocked_period);
if (empty($blocked_period['id'])) {
return $this->insert($blocked_period);
} else {
return $this->update($blocked_period);
}
}
/**
* Validate the blocked-period data.
*
* @param array $blocked_period Associative array with the blocked-period data.
*
* @throws InvalidArgumentException
* @throws Exception
*/
public function validate(array $blocked_period): void
{
// If a blocked-period ID is provided then check whether the record really exists in the database.
if (!empty($blocked_period['id'])) {
$count = $this->db->get_where('blocked_periods', ['id' => $blocked_period['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided blocked-period ID does not exist in the database: ' . $blocked_period['id'],
);
}
}
// Make sure all required fields are provided.
if (
empty($blocked_period['name']) ||
empty($blocked_period['start_datetime']) ||
empty($blocked_period['end_datetime'])
) {
throw new InvalidArgumentException(
'Not all required fields are provided: ' . print_r($blocked_period, true),
);
}
// Make sure that the start date time is before the end.
$start_date_time_object = new DateTime($blocked_period['start_datetime']);
$end_date_time_object = new DateTime($blocked_period['end_datetime']);
if ($start_date_time_object >= $end_date_time_object) {
throw new InvalidArgumentException('The start must be before the end date time value.');
}
}
/**
* Insert a new blocked-period into the database.
*
* @param array $blocked_period Associative array with the blocked-period data.
*
* @return int Returns the blocked-period ID.
*
* @throws RuntimeException
*/
protected function insert(array $blocked_period): int
{
$blocked_period['create_datetime'] = date('Y-m-d H:i:s');
$blocked_period['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->insert('blocked_periods', $blocked_period)) {
throw new RuntimeException('Could not insert blocked-period.');
}
return $this->db->insert_id();
}
/**
* Update an existing blocked-period.
*
* @param array $blocked_period Associative array with the blocked-period data.
*
* @return int Returns the blocked-period ID.
*
* @throws RuntimeException
*/
protected function update(array $blocked_period): int
{
$blocked_period['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('blocked_periods', $blocked_period, ['id' => $blocked_period['id']])) {
throw new RuntimeException('Could not update blocked periods.');
}
return $blocked_period['id'];
}
/**
* Remove an existing blocked-period from the database.
*
* @param int $blocked_period_id Blocked period ID.
*
* @throws RuntimeException
*/
public function delete(int $blocked_period_id): void
{
$this->db->delete('blocked_periods', ['id' => $blocked_period_id]);
}
/**
* Get a specific blocked-period from the database.
*
* @param int $blocked_period_id The ID of the record to be returned.
*
* @return array Returns an array with the blocked-period data.
*
* @throws InvalidArgumentException
*/
public function find(int $blocked_period_id): array
{
$blocked_period = $this->db->get_where('blocked_periods', ['id' => $blocked_period_id])->row_array();
if (!$blocked_period) {
throw new InvalidArgumentException(
'The provided blocked-period ID was not found in the database: ' . $blocked_period_id,
);
}
$this->cast($blocked_period);
return $blocked_period;
}
/**
* Get a specific field value from the database.
*
* @param int $blocked_period_id Blocked period ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected blocked-period value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $blocked_period_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($blocked_period_id)) {
throw new InvalidArgumentException('The blocked-period ID argument cannot be empty.');
}
// Check whether the service exists.
$query = $this->db->get_where('blocked_periods', ['id' => $blocked_period_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException(
'The provided blocked-period ID was not found in the database: ' . $blocked_period_id,
);
}
// Check if the required field is part of the blocked-period data.
$blocked_period = $query->row_array();
$this->cast($blocked_period);
if (!array_key_exists($field, $blocked_period)) {
throw new InvalidArgumentException(
'The requested field was not found in the blocked-period data: ' . $field,
);
}
return $blocked_period[$field];
}
/**
* Search blocked periods by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of blocked periods.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$blocked_periods = $this->db
->select()
->from('blocked_periods')
->group_start()
->like('name', $keyword)
->or_like('notes', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($blocked_periods as &$blocked_period) {
$this->cast($blocked_period);
}
return $blocked_periods;
}
/**
* Get all services that match the provided criteria.
*
* @param array|string|null $where Where conditions
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of blocked periods.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$blocked_periods = $this->db->get('blocked_periods', $limit, $offset)->result_array();
foreach ($blocked_periods as &$blocked_period) {
$this->cast($blocked_period);
}
return $blocked_periods;
}
/**
* Load related resources to a blocked-period.
*
* @param array $blocked_period Associative array with the blocked-period data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$blocked_period, array $resources)
{
// Blocked periods do not currently have any related resources.
}
/**
* Convert the database blocked-period record to the equivalent API resource.
*
* @param array $blocked_period Blocked period data.
*/
public function api_encode(array &$blocked_period): void
{
$encoded_resource = [
'id' => array_key_exists('id', $blocked_period) ? (int) $blocked_period['id'] : null,
'name' => $blocked_period['name'],
'start' => array_key_exists('start_datetime', $blocked_period) ? $blocked_period['start_datetime'] : null,
'end' => array_key_exists('end_datetime', $blocked_period) ? $blocked_period['end_datetime'] : null,
'notes' => array_key_exists('notes', $blocked_period) ? $blocked_period['notes'] : null,
];
$blocked_period = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database blocked-period record.
*
* @param array $blocked_period API resource.
* @param array|null $base Base blocked-period data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$blocked_period, ?array $base = null): void
{
$decoded_resource = $base ?: [];
if (array_key_exists('id', $blocked_period)) {
$decoded_resource['id'] = $blocked_period['id'];
}
if (array_key_exists('name', $blocked_period)) {
$decoded_resource['name'] = $blocked_period['name'];
}
if (array_key_exists('start', $blocked_period)) {
$decoded_resource['start_datetime'] = $blocked_period['start'];
}
if (array_key_exists('end', $blocked_period)) {
$decoded_resource['end_datetime'] = $blocked_period['end'];
}
if (array_key_exists('notes', $blocked_period)) {
$decoded_resource['notes'] = $blocked_period['notes'];
}
$blocked_period = $decoded_resource;
}
/**
* Get all the blocked periods that are within the provided period.
*
* @param string $start_date
* @param string $end_date
*
* @return array
*/
public function get_for_period(string $start_date, string $end_date): array
{
return $this->query()
//
->group_start()
->where('DATE(start_datetime) <=', $start_date)
->where('DATE(end_datetime) >=', $end_date)
->group_end()
//
->or_group_start()
->where('DATE(start_datetime) >=', $start_date)
->where('DATE(end_datetime) <=', $end_date)
->group_end()
//
->or_group_start()
->where('DATE(end_datetime) >', $start_date)
->where('DATE(end_datetime) <', $end_date)
->group_end()
//
->or_group_start()
->where('DATE(start_datetime) >', $start_date)
->where('DATE(start_datetime) <', $end_date)
->group_end()
//
->get()
->result_array();
}
/**
* Get the query builder interface, configured for use with the blocked periods table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('blocked_periods');
}
/**
* Check if a date is blocked by a blocked period.
*
* @param string $date
*
* @return bool
*/
public function is_entire_date_blocked(string $date): bool
{
return $this->query()
->where('DATE(start_datetime) <=', $date)
->where('DATE(end_datetime) >=', $date)
->get()
->num_rows() > 1;
}
}

View File

@@ -0,0 +1,265 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.3.2
* ---------------------------------------------------------------------------- */
/**
* Consents model.
*
* Handles all the database operations of the consent resource.
*
* @package Models
*/
class Consents_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
];
/**
* Save (insert or update) a consent.
*
* @param array $consent Associative array with the consent data.
*
* @return int Returns the consent ID.
*
* @throws InvalidArgumentException
*/
public function save(array $consent): int
{
$this->validate($consent);
if (empty($consent['id'])) {
return $this->insert($consent);
} else {
return $this->update($consent);
}
}
/**
* Validate the consent data.
*
* @param array $consent Associative array with the consent data.
*
* @throws InvalidArgumentException
*/
public function validate(array $consent): void
{
if (empty($consent['ip']) || empty($consent['type'])) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($consent, true));
}
}
/**
* Insert a new consent into the database.
*
* @param array $consent Associative array with the consent data.
*
* @return int Returns the consent ID.
*
* @throws RuntimeException
*/
protected function insert(array $consent): int
{
$consent['create_datetime'] = date('Y-m-d H:i:s');
$consent['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->insert('consents', $consent)) {
throw new RuntimeException('Could not insert consent.');
}
return $this->db->insert_id();
}
/**
* Update an existing consent.
*
* @param array $consent Associative array with the consent data.
*
* @return int Returns the consent ID.
*
* @throws RuntimeException
*/
protected function update(array $consent): int
{
$consent['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('consents', $consent, ['id' => $consent['id']])) {
throw new RuntimeException('Could not update consent.');
}
return $consent['id'];
}
/**
* Remove an existing consent from the database.
*
* @param int $consent_id Consent ID.
*
* @throws RuntimeException
*/
public function delete(int $consent_id): void
{
$this->db->delete('consents', ['id' => $consent_id]);
}
/**
* Get a specific consent from the database.
*
* @param int $consent_id The ID of the record to be returned.
*
* @return array Returns an array with the consent data.
*/
public function find(int $consent_id): array
{
$consent = $this->db->get_where('consents', ['id' => $consent_id])->row_array();
if (!$consent) {
throw new InvalidArgumentException('The provided consent ID was not found in the database: ' . $consent_id);
}
$this->cast($consent);
return $consent;
}
/**
* Get a specific field value from the database.
*
* @param int $consent_id Consent ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected consent value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $consent_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($consent_id)) {
throw new InvalidArgumentException('The consent ID argument cannot be empty.');
}
// Check whether the consent exists.
$query = $this->db->get_where('consents', ['id' => $consent_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException('The provided consent ID was not found in the database: ' . $consent_id);
}
// Check if the required field is part of the consent data.
$consent = $query->row_array();
$this->cast($consent);
if (!array_key_exists($field, $consent)) {
throw new InvalidArgumentException('The requested field was not found in the consent data: ' . $field);
}
return $consent[$field];
}
/**
* Get the query builder interface, configured for use with the consents table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('consents');
}
/**
* Search consents by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of consents.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$consents = $this->db
->select()
->from('consents')
->group_start()
->like('first_name', $keyword)
->or_like('last_name', $keyword)
->or_like('email', $keyword)
->or_like('ip', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($consents as &$consent) {
$this->cast($consent);
}
return $consents;
}
/**
* Get all consents that match the provided criteria.
*
* @param array|string|null $where Where conditions.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of consents.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$consents = $this->db->get('consents', $limit, $offset)->result_array();
foreach ($consents as &$consent) {
$this->cast($consent);
}
return $consents;
}
/**
* Load related resources to a consent.
*
* @param array $consent Associative array with the consent data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$consent, array $resources)
{
// Consents do not currently have any related resources.
}
}

View File

@@ -0,0 +1,552 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Customers model.
*
* Handles all the database operations of the customer resource.
*
* @package Models
*/
class Customers_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'id_roles' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'firstName' => 'first_name',
'lastName' => 'last_name',
'email' => 'email',
'phone' => 'phone_number',
'address' => 'address',
'city' => 'city',
'state' => 'state',
'zip' => 'zip_code',
'timezone' => 'timezone',
'language' => 'language',
'customField1' => 'custom_field_1',
'customField2' => 'custom_field_2',
'customField3' => 'custom_field_3',
'customField4' => 'custom_field_4',
'customField5' => 'custom_field_5',
'notes' => 'notes',
'ldapDn' => 'ldap_dn',
];
/**
* Save (insert or update) a customer.
*
* @param array $customer Associative array with the customer data.
*
* @return int Returns the customer ID.
*
* @throws InvalidArgumentException
*/
public function save(array $customer): int
{
$this->validate($customer);
if ($this->exists($customer) && empty($customer['id'])) {
$customer['id'] = $this->find_record_id($customer);
}
if (empty($customer['id'])) {
return $this->insert($customer);
} else {
return $this->update($customer);
}
}
/**
* Validate the customer data.
*
* @param array $customer Associative array with the customer data.
*
* @throws InvalidArgumentException
*/
public function validate(array $customer): void
{
// If a customer ID is provided then check whether the record really exists in the database.
if (!empty($customer['id'])) {
$count = $this->db->get_where('users', ['id' => $customer['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided customer ID does not exist in the database: ' . $customer['id'],
);
}
}
// Make sure all required fields are provided.
$require_first_name = filter_var(setting('require_first_name'), FILTER_VALIDATE_BOOLEAN);
$require_last_name = filter_var(setting('require_last_name'), FILTER_VALIDATE_BOOLEAN);
$require_email = filter_var(setting('require_email'), FILTER_VALIDATE_BOOLEAN);
$require_phone_number = filter_var(setting('require_phone_number'), FILTER_VALIDATE_BOOLEAN);
$require_address = filter_var(setting('require_address'), FILTER_VALIDATE_BOOLEAN);
$require_city = filter_var(setting('require_city'), FILTER_VALIDATE_BOOLEAN);
$require_zip_code = filter_var(setting('require_zip_code'), FILTER_VALIDATE_BOOLEAN);
if (
(empty($customer['first_name']) && $require_first_name) ||
(empty($customer['last_name']) && $require_last_name) ||
(empty($customer['email']) && $require_email) ||
(empty($customer['phone_number']) && $require_phone_number) ||
(empty($customer['address']) && $require_address) ||
(empty($customer['city']) && $require_city) ||
(empty($customer['zip_code']) && $require_zip_code)
) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($customer, true));
}
if (!empty($customer['email'])) {
// Validate the email address.
if (!filter_var($customer['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address provided: ' . $customer['email']);
}
// Make sure the email address is unique.
$customer_id = $customer['id'] ?? null;
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('roles.slug', DB_SLUG_CUSTOMER)
->where('users.email', $customer['email'])
->where('users.id !=', $customer_id)
->get()
->num_rows();
if ($count > 0) {
throw new InvalidArgumentException(
'The provided email address is already in use, please use a different one.',
);
}
}
}
/**
* Get all customers that match the provided criteria.
*
* @param array|string|null $where Where conditions.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of customers.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
$role_id = $this->get_customer_role_id();
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$customers = $this->db->get_where('users', ['id_roles' => $role_id], $limit, $offset)->result_array();
foreach ($customers as &$customer) {
$this->cast($customer);
}
return $customers;
}
/**
* Get the customer role ID.
*
* @return int Returns the role ID.
*/
public function get_customer_role_id(): int
{
$role = $this->db->get_where('roles', ['slug' => DB_SLUG_CUSTOMER])->row_array();
if (empty($role)) {
throw new RuntimeException('The customer role was not found in the database.');
}
return $role['id'];
}
/**
* Check if a particular customer record already exists in the database.
*
* @param array $customer Associative array with the customer data.
*
* @return bool Returns whether there is a record matching the provided one or not.
*
* @throws InvalidArgumentException
*/
public function exists(array $customer): bool
{
if (empty($customer['email'])) {
return false;
}
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('users.email', $customer['email'])
->where('roles.slug', DB_SLUG_CUSTOMER)
->get()
->num_rows();
return $count > 0;
}
/**
* Find the record ID of a customer.
*
* @param array $customer Associative array with the customer data.
*
* @return int Returns the ID of the record that matches the provided argument.
*
* @throws InvalidArgumentException
*/
public function find_record_id(array $customer): int
{
if (empty($customer['email'])) {
throw new InvalidArgumentException('The customer email was not provided: ' . print_r($customer, true));
}
$customer = $this->db
->select('users.id')
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('users.email', $customer['email'])
->where('roles.slug', DB_SLUG_CUSTOMER)
->get()
->row_array();
if (empty($customer)) {
throw new InvalidArgumentException('Could not find customer record id.');
}
return (int) $customer['id'];
}
/**
* Insert a new customer into the database.
*
* @param array $customer Associative array with the customer data.
*
* @return int Returns the customer ID.
*
* @throws RuntimeException
*/
protected function insert(array $customer): int
{
$customer['create_datetime'] = date('Y-m-d H:i:s');
$customer['update_datetime'] = date('Y-m-d H:i:s');
$customer['id_roles'] = $this->get_customer_role_id();
if (!$this->db->insert('users', $customer)) {
throw new RuntimeException('Could not insert customer.');
}
return $this->db->insert_id();
}
/**
* Update an existing customer.
*
* @param array $customer Associative array with the customer data.
*
* @return int Returns the customer ID.
*
* @throws RuntimeException
*/
protected function update(array $customer): int
{
$customer['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('users', $customer, ['id' => $customer['id']])) {
throw new RuntimeException('Could not update customer.');
}
return $customer['id'];
}
/**
* Remove an existing customer from the database.
*
* @param int $customer_id Customer ID.
*
* @throws RuntimeException
*/
public function delete(int $customer_id): void
{
$this->db->delete('users', ['id' => $customer_id]);
}
/**
* Get a specific customer from the database.
*
* @param int $customer_id The ID of the record to be returned.
*
* @return array Returns an array with the customer data.
*/
public function find(int $customer_id): array
{
$customer = $this->db->get_where('users', ['id' => $customer_id])->row_array();
if (!$customer) {
throw new InvalidArgumentException(
'The provided customer ID was not found in the database: ' . $customer_id,
);
}
$this->cast($customer);
return $customer;
}
/**
* Get a specific field value from the database.
*
* @param int $customer_id Customer ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected customer value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $customer_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($customer_id)) {
throw new InvalidArgumentException('The customer ID argument cannot be empty.');
}
// Check whether the customer exists.
$query = $this->db->get_where('users', ['id' => $customer_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException(
'The provided customer ID was not found in the database: ' . $customer_id,
);
}
// Check if the required field is part of the customer data.
$customer = $query->row_array();
$this->cast($customer);
if (!array_key_exists($field, $customer)) {
throw new InvalidArgumentException('The requested field was not found in the customer data: ' . $field);
}
return $customer[$field];
}
/**
* Get the query builder interface, configured for use with the users (customer-filtered) table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
$role_id = $this->get_customer_role_id();
return $this->db->from('users')->where('id_roles', $role_id);
}
/**
* Search customers by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of customers.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$role_id = $this->get_customer_role_id();
$customers = $this->db
->select()
->from('users')
->where('id_roles', $role_id)
->group_start()
->like('first_name', $keyword)
->or_like('last_name', $keyword)
->or_like('CONCAT_WS(" ", first_name, last_name)', $keyword)
->or_like('email', $keyword)
->or_like('phone_number', $keyword)
->or_like('mobile_number', $keyword)
->or_like('address', $keyword)
->or_like('city', $keyword)
->or_like('state', $keyword)
->or_like('zip_code', $keyword)
->or_like('notes', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($customers as &$customer) {
$this->cast($customer);
}
return $customers;
}
/**
* Load related resources to a customer.
*
* @param array $customer Associative array with the customer data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$customer, array $resources)
{
// Customers do not currently have any related resources.
}
/**
* Convert the database customer record to the equivalent API resource.
*
* @param array $customer Customer data.
*/
public function api_encode(array &$customer): void
{
$encoded_resource = [
'id' => array_key_exists('id', $customer) ? (int) $customer['id'] : null,
'firstName' => $customer['first_name'],
'lastName' => $customer['last_name'],
'email' => $customer['email'],
'phone' => $customer['phone_number'],
'address' => $customer['address'],
'city' => $customer['city'],
'zip' => $customer['zip_code'],
'notes' => $customer['notes'],
'timezone' => $customer['timezone'],
'language' => $customer['language'],
'customField1' => $customer['custom_field_1'],
'customField2' => $customer['custom_field_2'],
'customField3' => $customer['custom_field_3'],
'customField4' => $customer['custom_field_4'],
'customField5' => $customer['custom_field_5'],
'ldapDn' => $customer['ldap_dn'],
];
$customer = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database admin record.
*
* @param array $customer API resource.
* @param array|null $base Base customer data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$customer, ?array $base = null): void
{
$decoded_resource = $base ?: [];
if (array_key_exists('id', $customer)) {
$decoded_resource['id'] = $customer['id'];
}
if (array_key_exists('firstName', $customer)) {
$decoded_resource['first_name'] = $customer['firstName'];
}
if (array_key_exists('lastName', $customer)) {
$decoded_resource['last_name'] = $customer['lastName'];
}
if (array_key_exists('email', $customer)) {
$decoded_resource['email'] = $customer['email'];
}
if (array_key_exists('phone', $customer)) {
$decoded_resource['phone_number'] = $customer['phone'];
}
if (array_key_exists('address', $customer)) {
$decoded_resource['address'] = $customer['address'];
}
if (array_key_exists('city', $customer)) {
$decoded_resource['city'] = $customer['city'];
}
if (array_key_exists('zip', $customer)) {
$decoded_resource['zip_code'] = $customer['zip'];
}
if (array_key_exists('language', $customer)) {
$decoded_resource['language'] = $customer['language'];
}
if (array_key_exists('timezone', $customer)) {
$decoded_resource['timezone'] = $customer['timezone'];
}
if (array_key_exists('customField1', $customer)) {
$decoded_resource['custom_field_1'] = $customer['customField1'];
}
if (array_key_exists('customField2', $customer)) {
$decoded_resource['custom_field_2'] = $customer['customField2'];
}
if (array_key_exists('customField3', $customer)) {
$decoded_resource['custom_field_3'] = $customer['customField3'];
}
if (array_key_exists('customField4', $customer)) {
$decoded_resource['custom_field_4'] = $customer['customField4'];
}
if (array_key_exists('customField5', $customer)) {
$decoded_resource['custom_field_5'] = $customer['customField5'];
}
if (array_key_exists('ldapDn', $customer)) {
$decoded_resource['ldap_dn'] = $customer['ldapDn'];
}
if (array_key_exists('notes', $customer)) {
$decoded_resource['notes'] = $customer['notes'];
}
$customer = $decoded_resource;
}
}

View File

@@ -0,0 +1,985 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Providers model.
*
* Handles all the database operations of the provider resource.
*
* @package Models
*/
class Providers_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'is_private' => 'boolean',
'id_roles' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'firstName' => 'first_name',
'lastName' => 'last_name',
'email' => 'email',
'mobile' => 'mobile_number',
'phone' => 'phone_number',
'address' => 'address',
'city' => 'city',
'state' => 'state',
'zip' => 'zip_code',
'timezone' => 'timezone',
'language' => 'language',
'notes' => 'notes',
'isPrivate' => 'is_private',
'ldapDn' => 'ldap_dn',
'roleId' => 'id_roles',
];
/**
* Save (insert or update) a provider.
*
* @param array $provider Associative array with the provider data.
*
* @return int Returns the provider ID.
*
* @throws InvalidArgumentException
* @throws Exception
*/
public function save(array $provider): int
{
$this->validate($provider);
if (empty($provider['id'])) {
return $this->insert($provider);
} else {
return $this->update($provider);
}
}
/**
* Validate the provider data.
*
* @param array $provider Associative array with the provider data.
*
* @throws InvalidArgumentException
*/
public function validate(array $provider): void
{
// If a provider ID is provided then check whether the record really exists in the database.
if (!empty($provider['id'])) {
$count = $this->db->get_where('users', ['id' => $provider['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided provider ID does not exist in the database: ' . $provider['id'],
);
}
}
// Make sure all required fields are provided.
if (
empty($provider['first_name']) ||
empty($provider['last_name']) ||
empty($provider['email']) ||
empty($provider['phone_number'])
) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($provider, true));
}
// Validate the email address.
if (!filter_var($provider['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address provided: ' . $provider['email']);
}
// Validate provider services.
if (!empty($provider['services'])) {
// Make sure the provided service entries are numeric values.
foreach ($provider['services'] as $service_id) {
if (!is_numeric($service_id)) {
throw new InvalidArgumentException(
'The provided provider services are invalid: ' . print_r($provider, true),
);
}
}
}
// Make sure the username is unique.
if (!empty($provider['settings']['username'])) {
$provider_id = $provider['id'] ?? null;
if (!$this->validate_username($provider['settings']['username'], $provider_id)) {
throw new InvalidArgumentException(
'The provided username is already in use, please use a different one.',
);
}
}
// Validate the password.
if (!empty($provider['settings']['password'])) {
if (strlen($provider['settings']['password']) < MIN_PASSWORD_LENGTH) {
throw new InvalidArgumentException(
'The provider password must be at least ' . MIN_PASSWORD_LENGTH . ' characters long.',
);
}
}
// New users must always have a password value set.
if (empty($provider['id']) && empty($provider['settings']['password'])) {
throw new InvalidArgumentException('The provider password cannot be empty when inserting a new record.');
}
// Validate calendar view type value.
if (
!empty($provider['settings']['calendar_view']) &&
!in_array($provider['settings']['calendar_view'], [CALENDAR_VIEW_DEFAULT, CALENDAR_VIEW_TABLE])
) {
throw new InvalidArgumentException(
'The provided calendar view is invalid: ' . $provider['settings']['calendar_view'],
);
}
// Make sure the email address is unique.
$provider_id = $provider['id'] ?? null;
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('roles.slug', DB_SLUG_PROVIDER)
->where('users.email', $provider['email'])
->where('users.id !=', $provider_id)
->get()
->num_rows();
if ($count > 0) {
throw new InvalidArgumentException(
'The provided email address is already in use, please use a different one.',
);
}
}
/**
* Validate the provider username.
*
* @param string $username Provider username.
* @param int|null $provider_id Provider ID.
*
* @return bool Returns the validation result.
*/
public function validate_username(string $username, ?int $provider_id = null): bool
{
if (!empty($provider_id)) {
$this->db->where('id_users !=', $provider_id);
}
return $this->db
->from('users')
->join('user_settings', 'user_settings.id_users = users.id', 'inner')
->where(['username' => $username])
->get()
->num_rows() === 0;
}
/**
* Get all providers that match the provided criteria.
*
* @param array|string|null $where Where conditions
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of providers.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
$role_id = $this->get_provider_role_id();
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$providers = $this->db->get_where('users', ['id_roles' => $role_id], $limit, $offset)->result_array();
foreach ($providers as &$provider) {
$this->cast($provider);
$provider['settings'] = $this->get_settings($provider['id']);
$provider['services'] = $this->get_service_ids($provider['id']);
}
return $providers;
}
/**
* Get the provider role ID.
*
* @return int Returns the role ID.
*/
public function get_provider_role_id(): int
{
$role = $this->db->get_where('roles', ['slug' => DB_SLUG_PROVIDER])->row_array();
if (empty($role)) {
throw new RuntimeException('The provider role was not found in the database.');
}
return $role['id'];
}
/**
* Insert a new provider into the database.
*
* @param array $provider Associative array with the provider data.
*
* @return int Returns the provider ID.
*
* @throws RuntimeException|Exception
*/
protected function insert(array $provider): int
{
$provider['create_datetime'] = date('Y-m-d H:i:s');
$provider['update_datetime'] = date('Y-m-d H:i:s');
$provider['id_roles'] = $this->get_provider_role_id();
$service_ids = $provider['services'];
$settings = $provider['settings'];
unset($provider['services'], $provider['settings']);
if (!$this->db->insert('users', $provider)) {
throw new RuntimeException('Could not insert provider.');
}
$provider['id'] = $this->db->insert_id();
$settings['salt'] = generate_salt();
$settings['password'] = hash_password($settings['salt'], $settings['password']);
$this->set_settings($provider['id'], $settings);
$this->set_service_ids($provider['id'], $service_ids);
return $provider['id'];
}
/**
* Save the provider settings.
*
* @param int $provider_id Provider ID.
* @param array $settings Associative array with the settings data.
*
* @throws InvalidArgumentException
*/
public function set_settings(int $provider_id, array $settings): void
{
if (empty($settings)) {
throw new InvalidArgumentException('The settings argument cannot be empty.');
}
// Make sure the settings record exists in the database.
$count = $this->db->get_where('user_settings', ['id_users' => $provider_id])->num_rows();
if (!$count) {
$this->db->insert('user_settings', ['id_users' => $provider_id]);
}
foreach ($settings as $name => $value) {
// Sort working plans exceptions in descending order that they are easier to modify later on.
if ($name === 'working_plan_exceptions') {
$value = json_decode($value, true);
if (!$value) {
$value = [];
}
krsort($value);
$value = json_encode(empty($value) ? new stdClass() : $value);
}
$this->set_setting($provider_id, $name, $value);
}
}
/**
* Get the provider settings.
*
* @param int $provider_id Provider ID.
*
* @throws InvalidArgumentException
*/
public function get_settings(int $provider_id): array
{
$settings = $this->db->get_where('user_settings', ['id_users' => $provider_id])->row_array();
unset($settings['id_users'], $settings['password'], $settings['salt']);
return $settings;
}
/**
* Set the value of a provider setting.
*
* @param int $provider_id Provider ID.
* @param string $name Setting name.
* @param mixed|null $value Setting value.
*/
public function set_setting(int $provider_id, string $name, mixed $value = null): void
{
if (!$this->db->update('user_settings', [$name => $value], ['id_users' => $provider_id])) {
throw new RuntimeException('Could not set the new provider setting value: ' . $name);
}
}
/**
* Update an existing provider.
*
* @param array $provider Associative array with the provider data.
*
* @return int Returns the provider ID.
*
* @throws RuntimeException|Exception
*/
protected function update(array $provider): int
{
$provider['update_datetime'] = date('Y-m-d H:i:s');
$service_ids = $provider['services'];
$settings = $provider['settings'];
unset($provider['services'], $provider['settings']);
if (isset($settings['password'])) {
$existing_settings = $this->db->get_where('user_settings', ['id_users' => $provider['id']])->row_array();
if (empty($existing_settings)) {
throw new RuntimeException('No settings record found for provider with ID: ' . $provider['id']);
}
if (empty($existing_settings['salt'])) {
$existing_settings['salt'] = $settings['salt'] = generate_salt();
}
$settings['password'] = hash_password($existing_settings['salt'], $settings['password']);
}
if (!$this->db->update('users', $provider, ['id' => $provider['id']])) {
throw new RuntimeException('Could not update provider.');
}
$this->set_settings($provider['id'], $settings);
$this->set_service_ids($provider['id'], $service_ids);
return $provider['id'];
}
/**
* Save the provider service IDs.
*
* @param int $provider_id Provider ID.
* @param array $service_ids Service IDs.
*/
public function set_service_ids(int $provider_id, array $service_ids): void
{
// Re-insert the provider-service connections.
$this->db->delete('services_providers', ['id_users' => $provider_id]);
foreach ($service_ids as $service_id) {
$service_provider_connection = [
'id_users' => $provider_id,
'id_services' => $service_id,
];
$this->db->insert('services_providers', $service_provider_connection);
}
}
/**
* Get the provider service IDs.
*
* @param int $provider_id Provider ID.
*/
public function get_service_ids(int $provider_id): array
{
$service_provider_connections = $this->db
->get_where('services_providers', ['id_users' => $provider_id])
->result_array();
$service_ids = [];
foreach ($service_provider_connections as $service_provider_connection) {
$service_ids[] = (int) $service_provider_connection['id_services'];
}
return $service_ids;
}
/**
* Remove an existing provider from the database.
*
* @param int $provider_id Provider ID.
*
* @throws RuntimeException
*/
public function delete(int $provider_id): void
{
$this->db->delete('users', ['id' => $provider_id]);
}
/**
* Get a specific field value from the database.
*
* @param int $provider_id Provider ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected provider value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $provider_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($provider_id)) {
throw new InvalidArgumentException('The provider ID argument cannot be empty.');
}
// Check whether the provider exists.
$query = $this->db->get_where('users', ['id' => $provider_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException(
'The provided provider ID was not found in the database: ' . $provider_id,
);
}
// Check if the required field is part of the provider data.
$provider = $query->row_array();
$this->cast($provider);
if (!array_key_exists($field, $provider)) {
throw new InvalidArgumentException('The requested field was not found in the provider data: ' . $field);
}
return $provider[$field];
}
/**
* Get the value of a provider setting.
*
* @param int $provider_id Provider ID.
* @param string $name Setting name.
*
* @return string Returns the value of the requested user setting.
*/
public function get_setting(int $provider_id, string $name): string
{
$settings = $this->db->get_where('user_settings', ['id_users' => $provider_id])->row_array();
if (!array_key_exists($name, $settings)) {
throw new RuntimeException('The requested setting value was not found: ' . $provider_id);
}
return $settings[$name];
}
/**
* Save a new or existing working plan exception.
*
* @param int $provider_id Provider ID.
* @param string $date Working plan exception date (in YYYY-MM-DD format).
* @param array|null $working_plan_exception Associative array with the working plan exception data.
*
* @throws Exception
*/
public function save_working_plan_exception(
int $provider_id,
string $date,
?array $working_plan_exception = null,
): void {
// Validate the working plan exception data.
if (
!empty($working_plan_exception) &&
(empty($working_plan_exception['start']) || empty($working_plan_exception['end']))
) {
throw new InvalidArgumentException(
'Empty start and/or end time provided: ' . json_encode($working_plan_exception),
);
}
if (!empty($working_plan_exception['start']) && !empty($working_plan_exception['end'])) {
$start = date('H:i', strtotime($working_plan_exception['start']));
$end = date('H:i', strtotime($working_plan_exception['end']));
if ($start > $end) {
throw new InvalidArgumentException('Working plan exception start date must be before the end date.');
}
}
// Make sure the provider record exists.
$where = [
'id' => $provider_id,
'id_roles' => $this->db->get_where('roles', ['slug' => DB_SLUG_PROVIDER])->row()->id,
];
if ($this->db->get_where('users', $where)->num_rows() === 0) {
throw new InvalidArgumentException('Provider ID was not found in the database: ' . $provider_id);
}
$provider = $this->find($provider_id);
// Store the working plan exception.
$working_plan_exceptions = json_decode($provider['settings']['working_plan_exceptions'], true);
if (is_array($working_plan_exception) && !isset($working_plan_exception['breaks'])) {
$working_plan_exception['breaks'] = [];
}
$working_plan_exceptions[$date] = $working_plan_exception;
$provider['settings']['working_plan_exceptions'] = json_encode($working_plan_exceptions);
$this->update($provider);
}
/**
* Get a specific provider from the database.
*
* @param int $provider_id The ID of the record to be returned.
*
* @return array Returns an array with the provider data.
*
* @throws InvalidArgumentException
*/
public function find(int $provider_id): array
{
$provider = $this->db->get_where('users', ['id' => $provider_id])->row_array();
if (!$provider) {
throw new InvalidArgumentException(
'The provided provider ID was not found in the database: ' . $provider_id,
);
}
$this->cast($provider);
$provider['settings'] = $this->get_settings($provider['id']);
$provider['services'] = $this->get_service_ids($provider['id']);
return $provider;
}
/**
* Delete a provider working plan exception.
*
* @param string $date The working plan exception date (in YYYY-MM-DD format).
* @param int $provider_id The selected provider record id.
*
* @throws Exception If $provider_id argument is invalid.
*/
public function delete_working_plan_exception(int $provider_id, string $date): void
{
$provider = $this->find($provider_id);
$working_plan_exceptions = json_decode($provider['settings']['working_plan_exceptions'], true);
if (!array_key_exists($date, $working_plan_exceptions)) {
return; // The selected date does not exist in provider's settings.
}
unset($working_plan_exceptions[$date]);
$provider['settings']['working_plan_exceptions'] = empty($working_plan_exceptions)
? '{}'
: json_encode($working_plan_exceptions);
$this->update($provider);
}
/**
* Get all the provider records that are assigned to at least one service.
*
* @param bool $without_private Only include the public providers.
*
* @return array Returns an array of providers.
*/
public function get_available_providers(bool $without_private = false): array
{
if ($without_private) {
$this->db->where('users.is_private', false);
}
$providers = $this->db
->select('users.*')
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->join('services_providers', 'services_providers.id_users = users.id', 'inner')
->where('roles.slug', DB_SLUG_PROVIDER)
->order_by('first_name ASC, last_name ASC, email ASC')
->group_by('users.id')
->get()
->result_array();
foreach ($providers as &$provider) {
$this->cast($provider);
$provider['settings'] = $this->get_settings($provider['id']);
$provider['services'] = $this->get_service_ids($provider['id']);
}
return $providers;
}
/**
* Get the query builder interface, configured for use with the users (provider-filtered) table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
$role_id = $this->get_provider_role_id();
return $this->db->from('users')->where('id_roles', $role_id);
}
/**
* Search providers by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of providers.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$role_id = $this->get_provider_role_id();
$providers = $this->db
->select()
->from('users')
->where('id_roles', $role_id)
->group_start()
->like('first_name', $keyword)
->or_like('last_name', $keyword)
->or_like('CONCAT_WS(" ", first_name, last_name)', $keyword)
->or_like('email', $keyword)
->or_like('phone_number', $keyword)
->or_like('mobile_number', $keyword)
->or_like('address', $keyword)
->or_like('city', $keyword)
->or_like('state', $keyword)
->or_like('zip_code', $keyword)
->or_like('notes', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($providers as &$provider) {
$this->cast($provider);
$provider['settings'] = $this->get_settings($provider['id']);
$provider['services'] = $this->get_service_ids($provider['id']);
}
return $providers;
}
/**
* Load related resources to a provider.
*
* @param array $provider Associative array with the provider data.
* @param array $resources Resource names to be attached ("services" supported).
*
* @throws InvalidArgumentException
*/
public function load(array &$provider, array $resources): void
{
if (empty($provider) || empty($resources)) {
return;
}
foreach ($resources as $resource) {
$provider['services'] = match ($resource) {
'services' => $this->db
->select('services.*')
->from('services')
->join('services_providers', 'services_providers.id_services = services.id', 'inner')
->where('id_users', $provider['id'])
->get()
->result_array(),
default => throw new InvalidArgumentException(
'The requested provider relation is not supported: ' . $resource,
),
};
}
}
/**
* Convert the database provider record to the equivalent API resource.
*
* @param array $provider Provider data.
*/
public function api_encode(array &$provider): void
{
$encoded_resource = [
'id' => array_key_exists('id', $provider) ? (int) $provider['id'] : null,
'firstName' => $provider['first_name'],
'lastName' => $provider['last_name'],
'email' => $provider['email'],
'mobile' => $provider['mobile_number'],
'phone' => $provider['phone_number'],
'address' => $provider['address'],
'city' => $provider['city'],
'state' => $provider['state'],
'zip' => $provider['zip_code'],
'notes' => $provider['notes'],
'isPrivate' => $provider['is_private'],
'ldapDn' => $provider['ldap_dn'],
'timezone' => $provider['timezone'],
'language' => $provider['language'],
];
if (array_key_exists('services', $provider)) {
$encoded_resource['services'] = $provider['services'];
}
if (array_key_exists('settings', $provider)) {
$encoded_resource['settings'] = [
'username' => $provider['settings']['username'],
'notifications' => filter_var($provider['settings']['notifications'], FILTER_VALIDATE_BOOLEAN),
'calendarView' => $provider['settings']['calendar_view'],
'googleSync' => array_key_exists('google_sync', $provider['settings'])
? filter_var($provider['settings']['google_sync'], FILTER_VALIDATE_BOOLEAN)
: null,
'googleToken' => array_key_exists('google_token', $provider['settings'])
? $provider['settings']['google_token']
: null,
'googleCalendar' => array_key_exists('google_calendar', $provider['settings'])
? $provider['settings']['google_calendar']
: null,
'caldavSync' => array_key_exists('caldav_sync', $provider['settings'])
? filter_var($provider['settings']['caldav_sync'], FILTER_VALIDATE_BOOLEAN)
: null,
'caldavUrl' => array_key_exists('caldav_url', $provider['settings'])
? $provider['settings']['caldav_url']
: null,
'caldavUsername' => array_key_exists('caldav_username', $provider['settings'])
? $provider['settings']['caldav_username']
: null,
'caldavPassword' => array_key_exists('caldav_password', $provider['settings'])
? $provider['settings']['caldav_password']
: null,
'syncFutureDays' => array_key_exists('sync_future_days', $provider['settings'])
? (int) $provider['settings']['sync_future_days']
: null,
'syncPastDays' => array_key_exists('sync_past_days', $provider['settings'])
? (int) $provider['settings']['sync_past_days']
: null,
'workingPlan' => array_key_exists('working_plan', $provider['settings'])
? json_decode($provider['settings']['working_plan'], true)
: null,
'workingPlanExceptions' => array_key_exists('working_plan_exceptions', $provider['settings'])
? json_decode($provider['settings']['working_plan_exceptions'], true)
: null,
];
}
$provider = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database provider record.
*
* @param array $provider API resource.
* @param array|null $base Base provider data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$provider, ?array $base = null): void
{
$decoded_resource = $base ?: [];
if (array_key_exists('id', $provider)) {
$decoded_resource['id'] = $provider['id'];
}
if (array_key_exists('firstName', $provider)) {
$decoded_resource['first_name'] = $provider['firstName'];
}
if (array_key_exists('lastName', $provider)) {
$decoded_resource['last_name'] = $provider['lastName'];
}
if (array_key_exists('email', $provider)) {
$decoded_resource['email'] = $provider['email'];
}
if (array_key_exists('mobile', $provider)) {
$decoded_resource['mobile_number'] = $provider['mobile'];
}
if (array_key_exists('phone', $provider)) {
$decoded_resource['phone_number'] = $provider['phone'];
}
if (array_key_exists('address', $provider)) {
$decoded_resource['address'] = $provider['address'];
}
if (array_key_exists('city', $provider)) {
$decoded_resource['city'] = $provider['city'];
}
if (array_key_exists('state', $provider)) {
$decoded_resource['state'] = $provider['state'];
}
if (array_key_exists('zip', $provider)) {
$decoded_resource['zip_code'] = $provider['zip'];
}
if (array_key_exists('notes', $provider)) {
$decoded_resource['notes'] = $provider['notes'];
}
if (array_key_exists('timezone', $provider)) {
$decoded_resource['timezone'] = $provider['timezone'];
}
if (array_key_exists('language', $provider)) {
$decoded_resource['language'] = $provider['language'];
}
if (array_key_exists('services', $provider)) {
$decoded_resource['services'] = $provider['services'];
}
if (array_key_exists('isPrivate', $provider)) {
$decoded_resource['is_private'] = (bool) $provider['isPrivate'];
}
if (array_key_exists('ldapDn', $provider)) {
$decoded_resource['ldap_dn'] = $provider['ldapDn'];
}
if (array_key_exists('settings', $provider)) {
if (empty($decoded_resource['settings'])) {
$decoded_resource['settings'] = [];
}
if (array_key_exists('username', $provider['settings'])) {
$decoded_resource['settings']['username'] = $provider['settings']['username'];
}
if (array_key_exists('password', $provider['settings'])) {
$decoded_resource['settings']['password'] = $provider['settings']['password'];
}
if (array_key_exists('calendarView', $provider['settings'])) {
$decoded_resource['settings']['calendar_view'] = $provider['settings']['calendarView'];
}
if (array_key_exists('notifications', $provider['settings'])) {
$decoded_resource['settings']['notifications'] = filter_var(
$provider['settings']['notifications'],
FILTER_VALIDATE_BOOLEAN,
);
}
if (array_key_exists('googleSync', $provider['settings'])) {
$decoded_resource['settings']['google_sync'] = filter_var(
$provider['settings']['googleSync'],
FILTER_VALIDATE_BOOLEAN,
);
}
if (array_key_exists('googleCalendar', $provider['settings'])) {
$decoded_resource['settings']['google_calendar'] = $provider['settings']['googleCalendar'];
}
if (array_key_exists('googleToken', $provider['settings'])) {
$decoded_resource['settings']['google_token'] = $provider['settings']['googleToken'];
}
if (array_key_exists('caldavSync', $provider['settings'])) {
$decoded_resource['settings']['caldav_sync'] = $provider['settings']['caldavSync'];
}
if (array_key_exists('caldavUrl', $provider['settings'])) {
$decoded_resource['settings']['caldav_url'] = $provider['settings']['caldavUrl'];
}
if (array_key_exists('caldavUsername', $provider['settings'])) {
$decoded_resource['settings']['caldav_username'] = $provider['settings']['caldavUsername'];
}
if (array_key_exists('caldavPassword', $provider['settings'])) {
$decoded_resource['settings']['caldav_password'] = $provider['settings']['caldavPassword'];
}
if (array_key_exists('syncFutureDays', $provider['settings'])) {
$decoded_resource['settings']['sync_future_days'] = $provider['settings']['syncFutureDays'];
}
if (array_key_exists('syncPastDays', $provider['settings'])) {
$decoded_resource['settings']['sync_past_days'] = $provider['settings']['syncPastDays'];
}
if (array_key_exists('workingPlan', $provider['settings'])) {
$decoded_resource['settings']['working_plan'] = json_encode($provider['settings']['workingPlan']);
}
if (array_key_exists('workingPlanExceptions', $provider['settings'])) {
$decoded_resource['settings']['working_plan_exceptions'] = json_encode(
$provider['settings']['workingPlanExceptions'],
);
}
}
$provider = $decoded_resource;
}
/**
* Quickly check if a service is assigned to a provider.
*
* @param int $provider_id
* @param int $service_id
*
* @return bool
*/
public function is_service_supported(int $provider_id, int $service_id): bool
{
$provider = $this->find($provider_id);
return in_array($service_id, $provider['services']);
}
}

View File

@@ -0,0 +1,342 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Roles model.
*
* Handles all the database operations of the role resource.
*
* @package Models
*/
class Roles_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'is_admin' => 'boolean',
'appointments' => 'integer',
'customers' => 'integer',
'services' => 'integer',
'users' => 'integer',
'system_settings' => 'integer',
'user_settings' => 'integer',
];
/**
* Save (insert or update) a role.
*
* @param array $role Associative array with the role data.
*
* @return int Returns the role ID.
*
* @throws InvalidArgumentException
*/
public function save(array $role): int
{
$this->validate($role);
if (empty($role['id'])) {
return $this->insert($role);
} else {
return $this->update($role);
}
}
/**
* Validate the role data.
*
* @param array $role Associative array with the role data.
*
* @throws InvalidArgumentException
*/
public function validate(array $role): void
{
// If a role ID is provided then check whether the record really exists in the database.
if (!empty($role['id'])) {
$count = $this->db->get_where('roles', ['id' => $role['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided role ID does not exist in the database: ' . $role['id'],
);
}
}
// Make sure all required fields are provided.
if (empty($role['name'])) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($role, true));
}
}
/**
* Insert a new role into the database.
*
* @param array $role Associative array with the role data.
*
* @return int Returns the role ID.
*
* @throws RuntimeException
*/
protected function insert(array $role): int
{
$role['create_datetime'] = date('Y-m-d H:i:s');
$role['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->insert('roles', $role)) {
throw new RuntimeException('Could not insert role.');
}
return $this->db->insert_id();
}
/**
* Update an existing role.
*
* @param array $role Associative array with the role data.
*
* @return int Returns the role ID.
*
* @throws RuntimeException
*/
protected function update(array $role): int
{
$role['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('roles', $role, ['id' => $role['id']])) {
throw new RuntimeException('Could not update role.');
}
return $role['id'];
}
/**
* Remove an existing role from the database.
*
* @param int $role_id Role ID.
*
* @throws RuntimeException
*/
public function delete(int $role_id): void
{
$this->db->delete('roles', ['id' => $role_id]);
}
/**
* Get a specific role from the database.
*
* @param int $role_id The ID of the record to be returned.
*
* @return array Returns an array with the role data.
*
* @throws InvalidArgumentException
*/
public function find(int $role_id): array
{
$role = $this->db->get_where('roles', ['id' => $role_id])->row_array();
if (!$role) {
throw new InvalidArgumentException('The provided role ID was not found in the database: ' . $role_id);
}
$this->cast($role);
return $role;
}
/**
* Get a specific field value from the database.
*
* @param int $role_id Role ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected role value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $role_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($role_id)) {
throw new InvalidArgumentException('The role ID argument cannot be empty.');
}
// Check whether the role exists.
$query = $this->db->get_where('roles', ['id' => $role_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException('The provided role ID was not found in the database: ' . $role_id);
}
// Check if the required field is part of the role data.
$role = $query->row_array();
$this->cast($role);
if (!array_key_exists($field, $role)) {
throw new InvalidArgumentException('The requested field was not found in the role data: ' . $field);
}
return $role[$field];
}
/**
* Get the permissions array by role slug.
*
* The permission numbers are converted into boolean values of the four main actions:
*
* - view
* - add
* - edit
* - delete
*
* After checking each individual value, you can make sure if the user is able to perform each action or not.
*
* @param string $slug Role slug.
*
* @return array Returns the permissions value.
*/
public function get_permissions_by_slug(string $slug): array
{
$role = $this->db->get_where('roles', ['slug' => $slug])->row_array();
$this->cast($role);
unset($role['id'], $role['name'], $role['slug'], $role['is_admin']);
// Convert the integer values to boolean.
$permissions = [];
foreach ($role as $resource => $value) {
$permissions[$resource] = [
'view' => false,
'add' => false,
'edit' => false,
'delete' => false,
];
if ($value > 0) {
if ((int) ($value / PRIV_DELETE) === 1) {
$permissions[$resource]['delete'] = true;
$value -= PRIV_DELETE;
}
if ((int) ($value / PRIV_EDIT) === 1) {
$permissions[$resource]['edit'] = true;
$value -= PRIV_EDIT;
}
if ((int) ($value / PRIV_ADD) === 1) {
$permissions[$resource]['add'] = true;
}
$permissions[$resource]['view'] = true;
}
}
return $permissions;
}
/**
* Get the query builder interface, configured for use with the roles table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('roles');
}
/**
* Search roles by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of roles.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$roles = $this->db
->select()
->from('roles')
->group_start()
->like('name', $keyword)
->or_like('slug', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($roles as &$role) {
$this->cast($role);
}
return $roles;
}
/**
* Get all roles that match the provided criteria.
*
* @param array|string|null $where Where conditions
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of roles.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$roles = $this->db->get('roles', $limit, $offset)->result_array();
foreach ($roles as &$role) {
$this->cast($role);
}
return $roles;
}
/**
* Load related resources to a role.
*
* @param array $role Associative array with the role data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$role, array $resources)
{
// Roles do not currently have any related resources.
}
}

View File

@@ -0,0 +1,755 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Secretaries model.
*
* Handles all the database operations of the secretary resource.
*
* @package Models
*/
class Secretaries_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'id_roles' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'firstName' => 'first_name',
'lastName' => 'last_name',
'email' => 'email',
'mobile' => 'mobile_number',
'phone' => 'phone_number',
'address' => 'address',
'city' => 'city',
'state' => 'state',
'zip' => 'zip_code',
'timezone' => 'timezone',
'language' => 'language',
'notes' => 'notes',
'ldapDn' => 'ldap_dn',
'roleId' => 'id_roles',
];
/**
* Save (insert or update) a secretary.
*
* @param array $secretary Associative array with the secretary data.
*
* @return int Returns the secretary ID.
*
* @throws InvalidArgumentException
* @throws Exception
*/
public function save(array $secretary): int
{
$this->validate($secretary);
if (empty($secretary['id'])) {
return $this->insert($secretary);
} else {
return $this->update($secretary);
}
}
/**
* Validate the secretary data.
*
* @param array $secretary Associative array with the secretary data.
*
* @throws InvalidArgumentException
*/
public function validate(array $secretary): void
{
// If a secretary ID is provided then check whether the record really exists in the database.
if (!empty($secretary['id'])) {
$count = $this->db->get_where('users', ['id' => $secretary['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided secretary ID does not exist in the database: ' . $secretary['id'],
);
}
}
// Make sure all required fields are provided.
if (
empty($secretary['first_name']) ||
empty($secretary['last_name']) ||
empty($secretary['email']) ||
empty($secretary['phone_number'])
) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($secretary, true));
}
// Validate the email address.
if (!filter_var($secretary['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address provided: ' . $secretary['email']);
}
// Validate secretary providers.
if (!empty($secretary['providers'])) {
// Make sure the provided provider entries are numeric values.
foreach ($secretary['providers'] as $provider_id) {
if (!is_numeric($provider_id)) {
throw new InvalidArgumentException(
'The provided secretary providers are invalid: ' . print_r($secretary, true),
);
}
}
}
// Make sure the username is unique.
if (!empty($secretary['settings']['username'])) {
$secretary_id = $secretary['id'] ?? null;
if (!$this->validate_username($secretary['settings']['username'], $secretary_id)) {
throw new InvalidArgumentException(
'The provided username is already in use, please use a different one.',
);
}
}
// Validate the password.
if (!empty($secretary['settings']['password'])) {
if (strlen($secretary['settings']['password']) < MIN_PASSWORD_LENGTH) {
throw new InvalidArgumentException(
'The secretary password must be at least ' . MIN_PASSWORD_LENGTH . ' characters long.',
);
}
}
// New users must always have a password value set.
if (empty($secretary['id']) && empty($secretary['settings']['password'])) {
throw new InvalidArgumentException('The secretary password cannot be empty when inserting a new record.');
}
// Validate calendar view type value.
if (
!empty($secretary['settings']['calendar_view']) &&
!in_array($secretary['settings']['calendar_view'], [CALENDAR_VIEW_DEFAULT, CALENDAR_VIEW_TABLE])
) {
throw new InvalidArgumentException(
'The provided calendar view is invalid: ' . $secretary['settings']['calendar_view'],
);
}
// Make sure the email address is unique.
$secretary_id = $secretary['id'] ?? null;
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('roles.slug', DB_SLUG_SECRETARY)
->where('users.email', $secretary['email'])
->where('users.id !=', $secretary_id)
->get()
->num_rows();
if ($count > 0) {
throw new InvalidArgumentException(
'The provided email address is already in use, please use a different one.',
);
}
}
/**
* Validate the secretary username.
*
* @param string $username Secretary username.
* @param int|null $secretary_id Secretary ID.
*
* @return bool Returns the validation result.
*/
public function validate_username(string $username, ?int $secretary_id = null): bool
{
if (!empty($secretary_id)) {
$this->db->where('id_users !=', $secretary_id);
}
return $this->db
->from('users')
->join('user_settings', 'user_settings.id_users = users.id', 'inner')
->where(['username' => $username])
->get()
->num_rows() === 0;
}
/**
* Get all secretaries that match the provided criteria.
*
* @param array|string|null $where Where conditions
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of secretaries.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
$role_id = $this->get_secretary_role_id();
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$secretaries = $this->db->get_where('users', ['id_roles' => $role_id], $limit, $offset)->result_array();
foreach ($secretaries as &$secretary) {
$this->cast($secretary);
$secretary['settings'] = $this->get_settings($secretary['id']);
$secretary['providers'] = $this->get_provider_ids($secretary['id']);
}
return $secretaries;
}
/**
* Get the secretary role ID.
*
* @return int Returns the role ID.
*/
public function get_secretary_role_id(): int
{
$role = $this->db->get_where('roles', ['slug' => DB_SLUG_SECRETARY])->row_array();
if (empty($role)) {
throw new RuntimeException('The secretary role was not found in the database.');
}
return $role['id'];
}
/**
* Insert a new secretary into the database.
*
* @param array $secretary Associative array with the secretary data.
*
* @return int Returns the secretary ID.
*
* @throws RuntimeException|Exception
*/
protected function insert(array $secretary): int
{
$secretary['create_datetime'] = date('Y-m-d H:i:s');
$secretary['update_datetime'] = date('Y-m-d H:i:s');
$secretary['id_roles'] = $this->get_secretary_role_id();
$provider_ids = $secretary['providers'] ?? [];
$settings = $secretary['settings'];
unset($secretary['providers'], $secretary['settings']);
if (!$this->db->insert('users', $secretary)) {
throw new RuntimeException('Could not insert secretary.');
}
$secretary['id'] = $this->db->insert_id();
$settings['salt'] = generate_salt();
$settings['password'] = hash_password($settings['salt'], $settings['password']);
$this->set_settings($secretary['id'], $settings);
$this->set_provider_ids($secretary['id'], $provider_ids);
return $secretary['id'];
}
/**
* Set the secretary settings.
*
* @param int $secretary_id Secretary ID.
* @param array $settings Associative array with the settings data.
*
* @throws InvalidArgumentException
*/
public function set_settings(int $secretary_id, array $settings): void
{
if (empty($settings)) {
throw new InvalidArgumentException('The settings argument cannot be empty.');
}
// Make sure the settings record exists in the database.
$count = $this->db->get_where('user_settings', ['id_users' => $secretary_id])->num_rows();
if (!$count) {
$this->db->insert('user_settings', ['id_users' => $secretary_id]);
}
foreach ($settings as $name => $value) {
$this->set_setting($secretary_id, $name, $value);
}
}
/**
* Get the secretary settings.
*
* @param int $secretary_id Secretary ID.
*
* @throws InvalidArgumentException
*/
public function get_settings(int $secretary_id): array
{
$settings = $this->db->get_where('user_settings', ['id_users' => $secretary_id])->row_array();
unset($settings['id_users'], $settings['password'], $settings['salt']);
return $settings;
}
/**
* Set the value of a secretary setting.
*
* @param int $secretary_id Secretary ID.
* @param string $name Setting name.
* @param mixed|null $value Setting value.
*/
public function set_setting(int $secretary_id, string $name, mixed $value = null): void
{
if (!$this->db->update('user_settings', [$name => $value], ['id_users' => $secretary_id])) {
throw new RuntimeException('Could not set the new secretary setting value: ' . $name);
}
}
/**
* Update an existing secretary.
*
* @param array $secretary Associative array with the secretary data.
*
* @return int Returns the secretary ID.
*
* @throws RuntimeException|Exception
*/
protected function update(array $secretary): int
{
$secretary['update_datetime'] = date('Y-m-d H:i:s');
$provider_ids = $secretary['providers'] ?? [];
$settings = $secretary['settings'];
unset($secretary['providers'], $secretary['settings']);
if (isset($settings['password'])) {
$existing_settings = $this->db->get_where('user_settings', ['id_users' => $secretary['id']])->row_array();
if (empty($existing_settings)) {
throw new RuntimeException('No settings record found for secretary with ID: ' . $secretary['id']);
}
if (empty($existing_settings['salt'])) {
$existing_settings['salt'] = $settings['salt'] = generate_salt();
}
$settings['password'] = hash_password($existing_settings['salt'], $settings['password']);
}
if (!$this->db->update('users', $secretary, ['id' => $secretary['id']])) {
throw new RuntimeException('Could not update secretary.');
}
$this->set_settings($secretary['id'], $settings);
$this->set_provider_ids($secretary['id'], $provider_ids);
return (int) $secretary['id'];
}
/**
* Set the secretary provider IDs.
*
* @param int $secretary_id Secretary ID.
* @param array $provider_ids Provider IDs.
*/
public function set_provider_ids(int $secretary_id, array $provider_ids): void
{
// Re-insert the secretary-provider connections.
$this->db->delete('secretaries_providers', ['id_users_secretary' => $secretary_id]);
foreach ($provider_ids as $provider_id) {
$secretary_provider_connection = [
'id_users_secretary' => $secretary_id,
'id_users_provider' => $provider_id,
];
$this->db->insert('secretaries_providers', $secretary_provider_connection);
}
}
/**
* Get the secretary provider IDs.
*
* @param int $secretary_id Secretary ID.
*/
public function get_provider_ids(int $secretary_id): array
{
$secretary_provider_connections = $this->db
->get_where('secretaries_providers', ['id_users_secretary' => $secretary_id])
->result_array();
$provider_ids = [];
foreach ($secretary_provider_connections as $secretary_provider_connection) {
$provider_ids[] = (int) $secretary_provider_connection['id_users_provider'];
}
return $provider_ids;
}
/**
* Remove an existing secretary from the database.
*
* @param int $secretary_id Provider ID.
*
* @throws RuntimeException
*/
public function delete(int $secretary_id): void
{
$this->db->delete('users', ['id' => $secretary_id]);
}
/**
* Get a specific field value from the database.
*
* @param int $secretary_id Secretary ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected secretary value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $secretary_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($secretary_id)) {
throw new InvalidArgumentException('The secretary ID argument cannot be empty.');
}
// Check whether the secretary exists.
$query = $this->db->get_where('users', ['id' => $secretary_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException(
'The provided secretary ID was not found in the database: ' . $secretary_id,
);
}
// Check if the required field is part of the secretary data.
$secretary = $query->row_array();
if (!array_key_exists($field, $secretary)) {
throw new InvalidArgumentException('The requested field was not found in the secretary data: ' . $field);
}
return $secretary[$field];
}
/**
* Get the value of a secretary setting.
*
* @param int $secretary_id Secretary ID.
* @param string $name Setting name.
*
* @return string Returns the value of the requested user setting.
*/
public function get_setting(int $secretary_id, string $name): string
{
$settings = $this->db->get_where('user_settings', ['id_users' => $secretary_id])->row_array();
if (!array_key_exists($name, $settings)) {
throw new RuntimeException('The requested setting value was not found: ' . $secretary_id);
}
return $settings[$name];
}
/**
* Get the query builder interface, configured for use with the users (secretary-filtered) table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
$role_id = $this->get_secretary_role_id();
return $this->db->from('users')->where('id_roles', $role_id);
}
/**
* Search secretaries by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of secretaries.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$role_id = $this->get_secretary_role_id();
$secretaries = $this->db
->select()
->from('users')
->where('id_roles', $role_id)
->group_start()
->like('first_name', $keyword)
->or_like('last_name', $keyword)
->or_like('CONCAT_WS(" ", first_name, last_name)', $keyword)
->or_like('email', $keyword)
->or_like('phone_number', $keyword)
->or_like('mobile_number', $keyword)
->or_like('address', $keyword)
->or_like('city', $keyword)
->or_like('state', $keyword)
->or_like('zip_code', $keyword)
->or_like('notes', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($secretaries as &$secretary) {
$this->cast($secretary);
$secretary['settings'] = $this->get_settings($secretary['id']);
$secretary['providers'] = $this->get_provider_ids($secretary['id']);
}
return $secretaries;
}
/**
* Load related resources to a secretary.
*
* @param array $secretary Associative array with the secretary data.
* @param array $resources Resource names to be attached ("providers" supported).
*
* @throws InvalidArgumentException
*/
public function load(array &$secretary, array $resources): void
{
if (empty($secretary) || empty($resources)) {
return;
}
foreach ($resources as $resource) {
$secretary['providers'] = match ($resource) {
'providers' => $this->db
->select('users.*')
->from('users')
->join('secretaries_providers', 'secretaries_providers.id_users_provider = users.id', 'inner')
->where('id_users_secretary', $secretary['id'])
->get()
->result_array(),
default => throw new InvalidArgumentException(
'The requested secretary relation is not supported: ' . $resource,
),
};
}
}
/**
* Convert the database secretary record to the equivalent API resource.
*
* @param array $secretary Secretary data.
*/
public function api_encode(array &$secretary): void
{
$encoded_resource = [
'id' => array_key_exists('id', $secretary) ? (int) $secretary['id'] : null,
'firstName' => $secretary['first_name'],
'lastName' => $secretary['last_name'],
'email' => $secretary['email'],
'mobile' => $secretary['mobile_number'],
'phone' => $secretary['phone_number'],
'address' => $secretary['address'],
'city' => $secretary['city'],
'state' => $secretary['state'],
'zip' => $secretary['zip_code'],
'notes' => $secretary['notes'],
'providers' => $secretary['providers'],
'timezone' => $secretary['timezone'],
'language' => $secretary['language'],
'ldapDn' => $secretary['ldap_dn'],
'settings' => [
'username' => $secretary['settings']['username'],
'notifications' => filter_var($secretary['settings']['notifications'], FILTER_VALIDATE_BOOLEAN),
'calendarView' => $secretary['settings']['calendar_view'],
],
];
$secretary = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database secretary record.
*
* @param array $secretary API resource.
* @param array|null $base Base secretary data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$secretary, ?array $base = null): void
{
$decoded_resource = $base ?: [];
if (array_key_exists('id', $secretary)) {
$decoded_resource['id'] = $secretary['id'];
}
if (array_key_exists('firstName', $secretary)) {
$decoded_resource['first_name'] = $secretary['firstName'];
}
if (array_key_exists('lastName', $secretary)) {
$decoded_resource['last_name'] = $secretary['lastName'];
}
if (array_key_exists('email', $secretary)) {
$decoded_resource['email'] = $secretary['email'];
}
if (array_key_exists('mobile', $secretary)) {
$decoded_resource['mobile_number'] = $secretary['mobile'];
}
if (array_key_exists('phone', $secretary)) {
$decoded_resource['phone_number'] = $secretary['phone'];
}
if (array_key_exists('address', $secretary)) {
$decoded_resource['address'] = $secretary['address'];
}
if (array_key_exists('city', $secretary)) {
$decoded_resource['city'] = $secretary['city'];
}
if (array_key_exists('state', $secretary)) {
$decoded_resource['state'] = $secretary['state'];
}
if (array_key_exists('zip', $secretary)) {
$decoded_resource['zip_code'] = $secretary['zip'];
}
if (array_key_exists('notes', $secretary)) {
$decoded_resource['notes'] = $secretary['notes'];
}
if (array_key_exists('timezone', $secretary)) {
$decoded_resource['timezone'] = $secretary['timezone'];
}
if (array_key_exists('language', $secretary)) {
$decoded_resource['language'] = $secretary['language'];
}
if (array_key_exists('ldapDn', $secretary)) {
$decoded_resource['ldap_dn'] = $secretary['ldapDn'];
}
if (array_key_exists('providers', $secretary)) {
$decoded_resource['providers'] = $secretary['providers'];
}
if (array_key_exists('settings', $secretary)) {
if (empty($decoded_resource['settings'])) {
$decoded_resource['settings'] = [];
}
if (array_key_exists('username', $secretary['settings'])) {
$decoded_resource['settings']['username'] = $secretary['settings']['username'];
}
if (array_key_exists('password', $secretary['settings'])) {
$decoded_resource['settings']['password'] = $secretary['settings']['password'];
}
if (array_key_exists('notifications', $secretary['settings'])) {
$decoded_resource['settings']['notifications'] = filter_var(
$secretary['settings']['notifications'],
FILTER_VALIDATE_BOOLEAN,
);
}
if (array_key_exists('calendarView', $secretary['settings'])) {
$decoded_resource['settings']['calendar_view'] = $secretary['settings']['calendarView'];
}
}
$secretary = $decoded_resource;
}
/**
* Quickly check if a provider is assigned to a provider.
*
* @param int $secretary_id
* @param int $provider_id
*
* @return bool
*/
public function is_provider_supported(int $secretary_id, int $provider_id): bool
{
$secretary = $this->find($secretary_id);
return in_array($provider_id, $secretary['providers']);
}
/**
* Get a specific secretary from the database.
*
* @param int $secretary_id The ID of the record to be returned.
*
* @return array Returns an array with the secretary data.
*
* @throws InvalidArgumentException
*/
public function find(int $secretary_id): array
{
$secretary = $this->db->get_where('users', ['id' => $secretary_id])->row_array();
if (!$secretary) {
throw new InvalidArgumentException(
'The provided secretary ID was not found in the database: ' . $secretary_id,
);
}
$this->cast($secretary);
$secretary['settings'] = $this->get_settings($secretary['id']);
$secretary['providers'] = $this->get_provider_ids($secretary['id']);
return $secretary;
}
}

View File

@@ -0,0 +1,337 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Service-Categories model.
*
* Handles all the database operations of the service-category resource.
*
* @package Models
*/
class Service_categories_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'name' => 'name',
'description' => 'description',
];
/**
* Save (insert or update) a service-category.
*
* @param array $service_category Associative array with the service-category data.
*
* @return int Returns the service-category ID.
*
* @throws InvalidArgumentException
*/
public function save(array $service_category): int
{
$this->validate($service_category);
if (empty($service_category['id'])) {
return $this->insert($service_category);
} else {
return $this->update($service_category);
}
}
/**
* Validate the service-category data.
*
* @param array $service_category Associative array with the service-category data.
*
* @throws InvalidArgumentException
*/
public function validate(array $service_category): void
{
// If a service-category ID is provided then check whether the record really exists in the database.
if (!empty($service_category['id'])) {
$count = $this->db->get_where('service_categories', ['id' => $service_category['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided service-category ID does not exist in the database: ' . $service_category['id'],
);
}
}
// Make sure all required fields are provided.
if (empty($service_category['name'])) {
throw new InvalidArgumentException(
'Not all required fields are provided: ' . print_r($service_category, true),
);
}
}
/**
* Insert a new service-category into the database.
*
* @param array $service_category Associative array with the service-category data.
*
* @return int Returns the service-category ID.
*
* @throws RuntimeException
*/
protected function insert(array $service_category): int
{
$service_category['create_datetime'] = date('Y-m-d H:i:s');
$service_category['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->insert('service_categories', $service_category)) {
throw new RuntimeException('Could not insert service-category.');
}
return $this->db->insert_id();
}
/**
* Update an existing service-category.
*
* @param array $service_category Associative array with the service-category data.
*
* @return int Returns the service-category ID.
*
* @throws RuntimeException
*/
protected function update(array $service_category): int
{
$service_category['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('service_categories', $service_category, ['id' => $service_category['id']])) {
throw new RuntimeException('Could not update service categories.');
}
return $service_category['id'];
}
/**
* Remove an existing service-category from the database.
*
* @param int $service_category_id Service-Category ID.
*
* @throws RuntimeException
*/
public function delete(int $service_category_id): void
{
$this->db->delete('service_categories', ['id' => $service_category_id]);
}
/**
* Get a specific service-category from the database.
*
* @param int $service_category_id The ID of the record to be returned.
*
* @return array Returns an array with the service-category data.
*
* @throws InvalidArgumentException
*/
public function find(int $service_category_id): array
{
$service_category = $this->db->get_where('service_categories', ['id' => $service_category_id])->row_array();
if (!$service_category) {
throw new InvalidArgumentException(
'The provided service-category ID was not found in the database: ' . $service_category_id,
);
}
$this->cast($service_category);
return $service_category;
}
/**
* Get a specific field value from the database.
*
* @param int $service_category_id Service-Category ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected service-category value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $service_category_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($service_category_id)) {
throw new InvalidArgumentException('The service-category ID argument cannot be empty.');
}
// Check whether the service exists.
$query = $this->db->get_where('service_categories', ['id' => $service_category_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException(
'The provided service-category ID was not found in the database: ' . $service_category_id,
);
}
// Check if the required field is part of the service-category data.
$service_category = $query->row_array();
$this->cast($service_category);
if (!array_key_exists($field, $service_category)) {
throw new InvalidArgumentException(
'The requested field was not found in the service-category data: ' . $field,
);
}
return $service_category[$field];
}
/**
* Get the query builder interface, configured for use with the service categories table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('service_categories');
}
/**
* Search service categories by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of service categories.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$service_categories = $this->db
->select()
->from('service_categories')
->group_start()
->like('name', $keyword)
->or_like('description', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($service_categories as &$service_category) {
$this->cast($service_category);
}
return $service_categories;
}
/**
* Get all services that match the provided criteria.
*
* @param array|string|null $where Where conditions
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of service categories.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$service_categories = $this->db->get('service_categories', $limit, $offset)->result_array();
foreach ($service_categories as &$service_category) {
$this->cast($service_category);
}
return $service_categories;
}
/**
* Load related resources to a service-category.
*
* @param array $service_category Associative array with the service-category data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$service_category, array $resources)
{
// Service categories do not currently have any related resources.
}
/**
* Convert the database service-category record to the equivalent API resource.
*
* @param array $service_category Category data.
*/
public function api_encode(array &$service_category): void
{
$encoded_resource = [
'id' => array_key_exists('id', $service_category) ? (int) $service_category['id'] : null,
'name' => $service_category['name'],
'description' => array_key_exists('description', $service_category)
? $service_category['description']
: null,
];
$service_category = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database service-category record.
*
* @param array $service_category API resource.
* @param array|null $base Base service-category data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$service_category, ?array $base = null): void
{
$decoded_resource = $base ?: [];
if (array_key_exists('id', $service_category)) {
$decoded_resource['id'] = $service_category['id'];
}
if (array_key_exists('name', $service_category)) {
$decoded_resource['name'] = $service_category['name'];
}
if (array_key_exists('description', $service_category)) {
$decoded_resource['description'] = $service_category['description'];
}
$service_category = $decoded_resource;
}
}

View File

@@ -0,0 +1,484 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Services model.
*
* Handles all the database operations of the service resource.
*
* @package Models
*/
class Services_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'price' => 'float',
'attendants_number' => 'integer',
'is_private' => 'boolean',
'id_service_categories' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'name' => 'name',
'duration' => 'duration',
'price' => 'price',
'currency' => 'currency',
'description' => 'description',
'location' => 'location',
'color' => 'color',
'availabilitiesType' => 'availabilities_type',
'attendantsNumber' => 'attendants_number',
'isPrivate' => 'is_private',
'serviceCategoryId' => 'id_service_categories',
];
/**
* Save (insert or update) a service.
*
* @param array $service Associative array with the service data.
*
* @return int Returns the service ID.
*
* @throws InvalidArgumentException
*/
public function save(array $service): int
{
$this->validate($service);
if (empty($service['id'])) {
return $this->insert($service);
} else {
return $this->update($service);
}
}
/**
* Validate the service data.
*
* @param array $service Associative array with the service data.
*
* @throws InvalidArgumentException
*/
public function validate(array $service): void
{
// If a service ID is provided then check whether the record really exists in the database.
if (!empty($service['id'])) {
$count = $this->db->get_where('services', ['id' => $service['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided service ID does not exist in the database: ' . $service['id'],
);
}
}
// Make sure all required fields are provided.
if (empty($service['name'])) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($service, true));
}
// If a category was provided then make sure it really exists in the database.
if (!empty($service['id_service_categories'])) {
$count = $this->db
->get_where('service_categories', ['id' => $service['id_service_categories']])
->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided category ID was not found in the database: ' . $service['id_service_categories'],
);
}
}
// Make sure the duration value is valid.
if (!empty($service['duration'])) {
if ((int) $service['duration'] < EVENT_MINIMUM_DURATION) {
throw new InvalidArgumentException(
'The service duration cannot be less than ' . EVENT_MINIMUM_DURATION . ' minutes long.',
);
}
}
// Availabilities type must have the correct value.
if (
$service['availabilities_type'] !== null &&
$service['availabilities_type'] !== AVAILABILITIES_TYPE_FLEXIBLE &&
$service['availabilities_type'] !== AVAILABILITIES_TYPE_FIXED
) {
throw new InvalidArgumentException(
'Service availabilities type must be either ' .
AVAILABILITIES_TYPE_FLEXIBLE .
' or ' .
AVAILABILITIES_TYPE_FIXED .
' (given ' .
$service['availabilities_type'] .
')',
);
}
// Validate the availabilities type value.
if (
!empty($service['availabilities_type']) &&
!in_array($service['availabilities_type'], [AVAILABILITIES_TYPE_FLEXIBLE, AVAILABILITIES_TYPE_FIXED])
) {
throw new InvalidArgumentException(
'The provided availabilities type is invalid: ' . $service['availabilities_type'],
);
}
// Validate the attendants number value.
if (empty($service['attendants_number']) || (int) $service['attendants_number'] < 1) {
throw new InvalidArgumentException(
'The provided attendants number is invalid: ' . $service['attendants_number'],
);
}
}
/**
* Insert a new service into the database.
*
* @param array $service Associative array with the service data.
*
* @return int Returns the service ID.
*
* @throws RuntimeException
*/
protected function insert(array $service): int
{
$service['create_datetime'] = date('Y-m-d H:i:s');
$service['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->insert('services', $service)) {
throw new RuntimeException('Could not insert service.');
}
return $this->db->insert_id();
}
/**
* Update an existing service.
*
* @param array $service Associative array with the service data.
*
* @return int Returns the service ID.
*
* @throws RuntimeException
*/
protected function update(array $service): int
{
$service['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('services', $service, ['id' => $service['id']])) {
throw new RuntimeException('Could not update service.');
}
return $service['id'];
}
/**
* Remove an existing service from the database.
*
* @param int $service_id Service ID.
*
* @throws RuntimeException
*/
public function delete(int $service_id): void
{
$this->db->delete('services', ['id' => $service_id]);
}
/**
* Get a specific service from the database.
*
* @param int $service_id The ID of the record to be returned.
*
* @return array Returns an array with the service data.
*
* @throws InvalidArgumentException
*/
public function find(int $service_id): array
{
$service = $this->db->get_where('services', ['id' => $service_id])->row_array();
if (!$service) {
throw new InvalidArgumentException('The provided service ID was not found in the database: ' . $service_id);
}
$this->cast($service);
return $service;
}
/**
* Get a specific field value from the database.
*
* @param int $service_id Service ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected service value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $service_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($service_id)) {
throw new InvalidArgumentException('The service ID argument cannot be empty.');
}
// Check whether the service exists.
$query = $this->db->get_where('services', ['id' => $service_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException('The provided service ID was not found in the database: ' . $service_id);
}
// Check if the required field is part of the service data.
$service = $query->row_array();
$this->cast($service);
if (!array_key_exists($field, $service)) {
throw new InvalidArgumentException('The requested field was not found in the service data: ' . $field);
}
return $service[$field];
}
/**
* Get all the service records that are assigned to at least one provider.
*
* @param bool $without_private Only include the public services.
*
* @return array Returns an array of services.
*/
public function get_available_services(bool $without_private = false): array
{
if ($without_private) {
$this->db->where('services.is_private', false);
}
$services = $this->db
->distinct()
->select(
'services.*, service_categories.name AS service_category_name, service_categories.id AS service_category_id',
)
->from('services')
->join('services_providers', 'services_providers.id_services = services.id', 'inner')
->join('service_categories', 'service_categories.id = services.id_service_categories', 'left')
->order_by('name ASC')
->get()
->result_array();
foreach ($services as &$service) {
$this->cast($service);
}
return $services;
}
/**
* Get all services that match the provided criteria.
*
* @param array|string|null $where Where conditions
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of services.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$services = $this->db->get('services', $limit, $offset)->result_array();
foreach ($services as &$service) {
$this->cast($service);
}
return $services;
}
/**
* Get the query builder interface, configured for use with the services table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('services');
}
/**
* Search services by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of services.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$services = $this->db
->select()
->from('services')
->group_start()
->like('name', $keyword)
->or_like('description', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($services as &$service) {
$this->cast($service);
}
return $services;
}
/**
* Load related resources to a service.
*
* @param array $service Associative array with the service data.
* @param array $resources Resource names to be attached ("category" supported).
*
* @throws InvalidArgumentException
*/
public function load(array &$service, array $resources): void
{
if (empty($service) || empty($resources)) {
return;
}
foreach ($resources as $resource) {
$service['category'] = match ($resource) {
'category' => $this->db
->get_where('service_categories', [
'id' => $service['id_service_categories'] ?? ($service['serviceCategoryId'] ?? null),
])
->row_array(),
default => throw new InvalidArgumentException(
'The requested appointment relation is not supported: ' . $resource,
),
};
}
}
/**
* Convert the database service record to the equivalent API resource.
*
* @param array $service Service data.
*/
public function api_encode(array &$service): void
{
$encoded_resource = [
'id' => array_key_exists('id', $service) ? (int) $service['id'] : null,
'name' => $service['name'],
'duration' => (int) $service['duration'],
'price' => (float) $service['price'],
'currency' => $service['currency'],
'description' => $service['description'],
'location' => $service['location'],
'availabilitiesType' => $service['availabilities_type'],
'attendantsNumber' => (int) $service['attendants_number'],
'isPrivate' => (bool) $service['is_private'],
'serviceCategoryId' =>
$service['id_service_categories'] !== null ? (int) $service['id_service_categories'] : null,
];
$service = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database service record.
*
* @param array $service API resource.
* @param array|null $base Base service data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$service, ?array $base = null): void
{
$decoded_resource = $base ?: [];
if (array_key_exists('id', $service)) {
$decoded_resource['id'] = $service['id'];
}
if (array_key_exists('name', $service)) {
$decoded_resource['name'] = $service['name'];
}
if (array_key_exists('duration', $service)) {
$decoded_resource['duration'] = $service['duration'];
}
if (array_key_exists('price', $service)) {
$decoded_resource['price'] = $service['price'];
}
if (array_key_exists('currency', $service)) {
$decoded_resource['currency'] = $service['currency'];
}
if (array_key_exists('description', $service)) {
$decoded_resource['description'] = $service['description'];
}
if (array_key_exists('location', $service)) {
$decoded_resource['location'] = $service['location'];
}
if (array_key_exists('availabilitiesType', $service)) {
$decoded_resource['availabilities_type'] = $service['availabilitiesType'];
}
if (array_key_exists('attendantsNumber', $service)) {
$decoded_resource['attendants_number'] = $service['attendantsNumber'];
}
if (array_key_exists('serviceCategoryId', $service)) {
$decoded_resource['id_service_categories'] = $service['serviceCategoryId'];
}
if (array_key_exists('isPrivate', $service)) {
$decoded_resource['is_private'] = (bool) $service['isPrivate'];
}
$service = $decoded_resource;
}
}

View File

@@ -0,0 +1,321 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Settings model.
*
* Handles all the database operations of the setting resource.
*
* @package Models
*/
class Settings_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'name' => 'name',
'value' => 'value',
];
/**
* Save (insert or update) a setting.
*
* @param array $setting Associative array with the setting data.
*
* @return int Returns the setting ID.
*
* @throws InvalidArgumentException
*/
public function save(array $setting): int
{
$this->validate($setting);
if (empty($setting['id'])) {
return $this->insert($setting);
} else {
return $this->update($setting);
}
}
/**
* Validate the setting data.
*
* @param array $setting Associative array with the setting data.
*
* @throws InvalidArgumentException
*/
public function validate(array $setting): void
{
// If a setting ID is provided then check whether the record really exists in the database.
if (!empty($setting['id'])) {
$count = $this->db->get_where('settings', ['id' => $setting['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided setting ID does not exist in the database: ' . $setting['id'],
);
}
}
// Make sure all required fields are provided.
if (empty($setting['name'])) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($setting, true));
}
}
/**
* Insert a new setting into the database.
*
* @param array $setting Associative array with the setting data.
*
* @return int Returns the setting ID.
*
* @throws RuntimeException
*/
protected function insert(array $setting): int
{
$setting['create_datetime'] = date('Y-m-d H:i:s');
$setting['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->insert('settings', $setting)) {
throw new RuntimeException('Could not insert setting.');
}
return $this->db->insert_id();
}
/**
* Update an existing setting.
*
* @param array $setting Associative array with the setting data.
*
* @return int Returns the setting ID.
*
* @throws RuntimeException
*/
protected function update(array $setting): int
{
$setting['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('settings', $setting, ['id' => $setting['id']])) {
throw new RuntimeException('Could not update setting.');
}
return $setting['id'];
}
/**
* Remove an existing setting from the database.
*
* @param int $setting_id Setting ID.
*
* @throws RuntimeException
*/
public function delete(int $setting_id): void
{
$this->db->delete('settings', ['id' => $setting_id]);
}
/**
* Get a specific setting from the database.
*
* @param int $setting_id The ID of the record to be returned.
*
* @return array Returns an array with the setting data.
*
* @throws InvalidArgumentException
*/
public function find(int $setting_id): array
{
$setting = $this->db->get_where('settings', ['id' => $setting_id])->row_array();
if (!$setting) {
throw new InvalidArgumentException('The provided setting ID was not found in the database: ' . $setting_id);
}
$this->cast($setting);
return $setting;
}
/**
* Get a specific field value from the database.
*
* @param int $setting_id Setting ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected setting value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $setting_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($setting_id)) {
throw new InvalidArgumentException('The setting ID argument cannot be empty.');
}
// Check whether the setting exists.
$query = $this->db->get_where('settings', ['id' => $setting_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException('The provided setting ID was not found in the database: ' . $setting_id);
}
// Check if the required field is part of the setting data.
$setting = $query->row_array();
$this->cast($setting);
if (!array_key_exists($field, $setting)) {
throw new InvalidArgumentException('The requested field was not found in the setting data: ' . $field);
}
return $setting[$field];
}
/**
* Get the query builder interface, configured for use with the settings table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('settings');
}
/**
* Search settings by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of settings.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$settings = $this->db
->select()
->from('settings')
->group_start()
->like('name', $keyword)
->or_like('value', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($settings as &$setting) {
$this->cast($setting);
}
return $settings;
}
/**
* Get all settings that match the provided criteria.
*
* @param array|string|null $where Where conditions
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of settings.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$settings = $this->db->get('settings', $limit, $offset)->result_array();
foreach ($settings as &$setting) {
$this->cast($setting);
}
return $settings;
}
/**
* Load related resources to a setting.
*
* @param array $setting Associative array with the setting data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$setting, array $resources)
{
// Users do not currently have any related resources.
}
/**
* Convert the database setting record to the equivalent API resource.
*
* @param array $setting Setting data.
*/
public function api_encode(array &$setting): void
{
$encoded_resource = [
'name' => $setting['name'],
'value' => $setting['value'],
];
$setting = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database setting record.
*
* @param array $setting API resource.
* @param array|null $base Base setting data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$setting, ?array $base = null): void
{
$decoded_resource = $base ?: [];
if (array_key_exists('name', $setting)) {
$decoded_resource['name'] = $setting['name'];
}
if (array_key_exists('value', $setting)) {
$decoded_resource['value'] = $setting['value'];
}
$setting = $decoded_resource;
}
}

View File

@@ -0,0 +1,446 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link http://easyunavailabilities.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Unavailabilities model.
*
* @package Models
*/
class Unavailabilities_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'is_unavailability' => 'boolean',
'id_users_provider' => 'integer',
'id_users_customer' => 'integer',
'id_services' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'book' => 'book_datetime',
'start' => 'start_datetime',
'end' => 'end_datetime',
'location' => 'location',
'color' => 'color',
'status' => 'status',
'notes' => 'notes',
'hash' => 'hash',
'providerId' => 'id_users_provider',
'googleCalendarId' => 'id_google_calendar',
];
/**
* Save (insert or update) an unavailability.
*
* @param array $unavailability Associative array with the unavailability data.
*
* @return int Returns the unavailability ID.
*
* @throws InvalidArgumentException
*/
public function save(array $unavailability): int
{
$this->validate($unavailability);
if (empty($unavailability['id'])) {
return $this->insert($unavailability);
} else {
return $this->update($unavailability);
}
}
/**
* Validate the unavailability data.
*
* @param array $unavailability Associative array with the unavailability data.
*
* @throws InvalidArgumentException
*/
public function validate(array $unavailability): void
{
// If an unavailability ID is provided then check whether the record really exists in the database.
if (!empty($unavailability['id'])) {
$count = $this->db->get_where('appointments', ['id' => $unavailability['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided unavailability ID does not exist in the database: ' . $unavailability['id'],
);
}
}
// Make sure all required fields are provided.
if (
empty($unavailability['start_datetime']) ||
empty($unavailability['end_datetime']) ||
empty($unavailability['id_users_provider'])
) {
throw new InvalidArgumentException(
'Not all required fields are provided: ' . print_r($unavailability, true),
);
}
// Make sure that the provided unavailability date time values are valid.
if (!validate_datetime($unavailability['start_datetime'])) {
throw new InvalidArgumentException('The unavailability start date time is invalid.');
}
if (!validate_datetime($unavailability['end_datetime'])) {
throw new InvalidArgumentException('The unavailability end date time is invalid.');
}
// Make the unavailability lasts longer than the minimum duration (in minutes).
$diff = (strtotime($unavailability['end_datetime']) - strtotime($unavailability['start_datetime'])) / 60;
if ($diff < EVENT_MINIMUM_DURATION) {
throw new InvalidArgumentException(
'The unavailability duration cannot be less than ' . EVENT_MINIMUM_DURATION . ' minutes.',
);
}
// Make sure the provider ID really exists in the database.
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
->where('users.id', $unavailability['id_users_provider'])
->where('roles.slug', DB_SLUG_PROVIDER)
->get()
->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The unavailability provider ID was not found in the database: ' . $unavailability['id_users_provider'],
);
}
}
/**
* Get all unavailabilities that match the provided criteria.
*
* @param array|string|null $where Where conditions.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of unavailabilities.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by) {
$this->db->order_by($order_by);
}
$unavailabilities = $this->db
->get_where('appointments', ['is_unavailability' => true], $limit, $offset)
->result_array();
foreach ($unavailabilities as &$unavailability) {
$this->cast($unavailability);
}
return $unavailabilities;
}
/**
* Insert a new unavailability into the database.
*
* @param array $unavailability Associative array with the unavailability data.
*
* @return int Returns the unavailability ID.
*
* @throws RuntimeException
*/
protected function insert(array $unavailability): int
{
$unavailability['book_datetime'] = date('Y-m-d H:i:s');
$unavailability['create_datetime'] = date('Y-m-d H:i:s');
$unavailability['update_datetime'] = date('Y-m-d H:i:s');
$unavailability['hash'] = random_string('alnum', 12);
$unavailability['is_unavailability'] = true;
if (!$this->db->insert('appointments', $unavailability)) {
throw new RuntimeException('Could not insert unavailability.');
}
return $this->db->insert_id();
}
/**
* Update an existing unavailability.
*
* @param array $unavailability Associative array with the unavailability data.
*
* @return int Returns the unavailability ID.
*
* @throws RuntimeException
*/
protected function update(array $unavailability): int
{
$unavailability['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('appointments', $unavailability, ['id' => $unavailability['id']])) {
throw new RuntimeException('Could not update unavailability record.');
}
return $unavailability['id'];
}
/**
* Remove an existing unavailability from the database.
*
* @param int $unavailability_id Unavailability ID.
*
* @throws RuntimeException
*/
public function delete(int $unavailability_id): void
{
$this->db->delete('appointments', ['id' => $unavailability_id]);
}
/**
* Get a specific unavailability from the database.
*
* @param int $unavailability_id The ID of the record to be returned.
*
* @return array Returns an array with the unavailability data.
*
* @throws InvalidArgumentException
*/
public function find(int $unavailability_id): array
{
$unavailability = $this->db->get_where('appointments', ['id' => $unavailability_id])->row_array();
if (!$unavailability) {
throw new InvalidArgumentException(
'The provided unavailability ID was not found in the database: ' . $unavailability_id,
);
}
$this->cast($unavailability);
return $unavailability;
}
/**
* Get a specific field value from the database.
*
* @param int $unavailability_id Unavailability ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected unavailability value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $unavailability_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($unavailability_id)) {
throw new InvalidArgumentException('The unavailability ID argument cannot be empty.');
}
// Check whether the unavailability exists.
$query = $this->db->get_where('appointments', ['id' => $unavailability_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException(
'The provided unavailability ID was not found in the database: ' . $unavailability_id,
);
}
// Check if the required field is part of the unavailability data.
$unavailability = $query->row_array();
$this->cast($unavailability);
if (!array_key_exists($field, $unavailability)) {
throw new InvalidArgumentException(
'The requested field was not found in the unavailability data: ' . $field,
);
}
return $unavailability[$field];
}
/**
* Get the query builder interface, configured for use with the unavailabilities table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('appointments');
}
/**
* Search unavailabilities by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of unavailabilities.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$unavailabilities = $this->db
->select()
->from('appointments')
->join('users AS providers', 'providers.id = appointments.id_users_provider', 'inner')
->where('is_unavailability', true)
->group_start()
->like('appointments.start_datetime', $keyword)
->or_like('appointments.end_datetime', $keyword)
->or_like('appointments.location', $keyword)
->or_like('appointments.hash', $keyword)
->or_like('appointments.notes', $keyword)
->or_like('providers.first_name', $keyword)
->or_like('providers.last_name', $keyword)
->or_like('providers.email', $keyword)
->or_like('providers.phone_number', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($unavailabilities as &$unavailability) {
$this->cast($unavailability);
}
return $unavailabilities;
}
/**
* Load related resources to an unavailability.
*
* @param array $unavailability Associative array with the unavailability data.
* @param array $resources Resource names to be attached ("service", "provider", "customer" supported).
*
* @throws InvalidArgumentException
*/
public function load(array &$unavailability, array $resources): void
{
if (empty($unavailability) || empty($resources)) {
return;
}
foreach ($resources as $resource) {
$unavailability['provider'] = match ($resource) {
'provider' => $this->db
->get_where('users', [
'id' => $unavailability['id_users_provider'] ?? ($unavailability['providerId'] ?? null),
])
->row_array(),
default => throw new InvalidArgumentException(
'The requested unavailability relation is not supported: ' . $resource,
),
};
}
}
/**
* Convert the database unavailability record to the equivalent API resource.
*
* @param array $unavailability Unavailability data.
*/
public function api_encode(array &$unavailability): void
{
$encoded_resource = [
'id' => array_key_exists('id', $unavailability) ? (int) $unavailability['id'] : null,
'book' => $unavailability['book_datetime'],
'start' => $unavailability['start_datetime'],
'end' => $unavailability['end_datetime'],
'hash' => $unavailability['hash'],
'location' => $unavailability['location'],
'notes' => $unavailability['notes'],
'providerId' =>
$unavailability['id_users_provider'] !== null ? (int) $unavailability['id_users_provider'] : null,
'googleCalendarId' =>
$unavailability['id_google_calendar'] !== null ? (int) $unavailability['id_google_calendar'] : null,
];
$unavailability = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database unavailability record.
*
* @param array $unavailability API resource.
* @param array|null $base Base unavailability data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$unavailability, ?array $base = null): void
{
$decoded_request = $base ?: [];
if (array_key_exists('id', $unavailability)) {
$decoded_request['id'] = $unavailability['id'];
}
if (array_key_exists('book', $unavailability)) {
$decoded_request['book_datetime'] = $unavailability['book'];
}
if (array_key_exists('start', $unavailability)) {
$decoded_request['start_datetime'] = $unavailability['start'];
}
if (array_key_exists('end', $unavailability)) {
$decoded_request['end_datetime'] = $unavailability['end'];
}
if (array_key_exists('hash', $unavailability)) {
$decoded_request['hash'] = $unavailability['hash'];
}
if (array_key_exists('location', $unavailability)) {
$decoded_request['location'] = $unavailability['location'];
}
if (array_key_exists('notes', $unavailability)) {
$decoded_request['notes'] = $unavailability['notes'];
}
if (array_key_exists('providerId', $unavailability)) {
$decoded_request['id_users_provider'] = $unavailability['providerId'];
}
if (array_key_exists('googleCalendarId', $unavailability)) {
$decoded_request['id_google_calendar'] = $unavailability['googleCalendarId'];
}
$decoded_request['is_unavailability'] = true;
$unavailability = $decoded_request;
}
}

View File

@@ -0,0 +1,433 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.0.0
* ---------------------------------------------------------------------------- */
/**
* Users model.
*
* Handles all the database operations of the user resource.
*
* @package Models
*/
class Users_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'id_roles' => 'integer',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'firstName' => 'first_name',
'lastName' => 'last_name',
'email' => 'email',
'mobile' => 'mobile_number',
'phone' => 'phone_number',
'address' => 'address',
'city' => 'city',
'state' => 'state',
'zip' => 'zip_code',
'timezone' => 'timezone',
'language' => 'language',
'ldapDn' => 'ldap_dn',
'notes' => 'notes',
'roleId' => 'id_roles',
];
/**
* Save (insert or update) a user.
*
* @param array $user Associative array with the user data.
*
* @return int Returns the user ID.
*
* @throws InvalidArgumentException
* @throws Exception
*/
public function save(array $user): int
{
$this->validate($user);
if (empty($user['id'])) {
return $this->insert($user);
} else {
return $this->update($user);
}
}
/**
* Validate the user data.
*
* @param array $user Associative array with the user data.
*
* @throws InvalidArgumentException
*/
public function validate(array $user): void
{
// If a user ID is provided then check whether the record really exists in the database.
if (!empty($user['id'])) {
$count = $this->db->get_where('users', ['id' => $user['id']])->num_rows();
if (!$count) {
throw new InvalidArgumentException(
'The provided user ID does not exist in the database: ' . $user['id'],
);
}
}
// Make sure all required fields are provided.
if (
empty($user['first_name']) ||
empty($user['last_name']) ||
empty($user['email']) ||
empty($user['phone_number'])
) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($user, true));
}
}
/**
* Insert a new user into the database.
*
* @param array $user Associative array with the user data.
*
* @return int Returns the user ID.
*
* @throws RuntimeException|Exception
*/
protected function insert(array $user): int
{
$user['create_datetime'] = date('Y-m-d H:i:s');
$user['update_datetime'] = date('Y-m-d H:i:s');
$settings = $user['settings'];
unset($user['settings']);
if (!$this->db->insert('users', $user)) {
throw new RuntimeException('Could not insert user.');
}
$user['id'] = $this->db->insert_id();
$settings['salt'] = generate_salt();
$settings['password'] = hash_password($settings['salt'], $settings['password']);
$this->set_settings($user['id'], $settings);
return $user['id'];
}
/**
* Save the user settings.
*
* @param int $user_id User ID.
* @param array $settings Associative array with the settings data.
*
* @throws InvalidArgumentException
*/
protected function set_settings(int $user_id, array $settings): void
{
if (empty($settings)) {
throw new InvalidArgumentException('The settings argument cannot be empty.');
}
// Make sure the settings record exists in the database.
$count = $this->db->get_where('user_settings', ['id_users' => $user_id])->num_rows();
if (!$count) {
$this->db->insert('user_settings', ['id_users' => $user_id]);
}
foreach ($settings as $name => $value) {
$this->set_setting($user_id, $name, $value);
}
}
/**
* Get the user settings.
*
* @param int $user_id User ID.
*
* @throws InvalidArgumentException
*/
public function get_settings(int $user_id): array
{
$settings = $this->db->get_where('user_settings', ['id_users' => $user_id])->row_array();
unset($settings['id_users'], $settings['password'], $settings['salt']);
return $settings;
}
/**
* Set the value of a user setting.
*
* @param int $user_id User ID.
* @param string $name Setting name.
* @param string $value Setting value.
*/
public function set_setting(int $user_id, string $name, string $value): void
{
if (!$this->db->update('user_settings', [$name => $value], ['id_users' => $user_id])) {
throw new RuntimeException('Could not set the new user setting value: ' . $name);
}
}
/**
* Update an existing user.
*
* @param array $user Associative array with the user data.
*
* @return int Returns the user ID.
*
* @throws RuntimeException|Exception
*/
protected function update(array $user): int
{
$user['update_datetime'] = date('Y-m-d H:i:s');
$settings = $user['settings'];
unset($user['settings']);
if (isset($settings['password'])) {
$existing_settings = $this->db->get_where('user_settings', ['id_users' => $user['id']])->row_array();
if (empty($existing_settings)) {
throw new RuntimeException('No settings record found for user with ID: ' . $user['id']);
}
$settings['password'] = hash_password($existing_settings['salt'], $settings['password']);
}
if (!$this->db->update('users', $user, ['id' => $user['id']])) {
throw new RuntimeException('Could not update user.');
}
$this->set_settings($user['id'], $settings);
return $user['id'];
}
/**
* Remove an existing user from the database.
*
* @param int $user_id User ID.
*
* @throws RuntimeException
*/
public function delete(int $user_id): void
{
$this->db->delete('users', ['id' => $user_id]);
}
/**
* Get a specific user from the database.
*
* @param int $user_id The ID of the record to be returned.
*
* @return array Returns an array with the user data.
*
* @throws InvalidArgumentException
*/
public function find(int $user_id): array
{
$user = $this->db->get_where('users', ['id' => $user_id])->row_array();
if (!$user) {
throw new InvalidArgumentException('The provided user ID was not found in the database: ' . $user_id);
}
$this->cast($user);
$user['settings'] = $this->get_settings($user['id']);
return $user;
}
/**
* Get a specific field value from the database.
*
* @param int $user_id User ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected user value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $user_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($user_id)) {
throw new InvalidArgumentException('The user ID argument cannot be empty.');
}
// Check whether the user exists.
$query = $this->db->get_where('users', ['id' => $user_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException('The provided user ID was not found in the database: ' . $user_id);
}
// Check if the required field is part of the user data.
$user = $query->row_array();
$this->cast($user);
if (!array_key_exists($field, $user)) {
throw new InvalidArgumentException('The requested field was not found in the user data: ' . $field);
}
return $user[$field];
}
/**
* Get the value of a user setting.
*
* @param int $user_id User ID.
* @param string $name Setting name.
*
* @return string Returns the value of the requested user setting.
*/
public function get_setting(int $user_id, string $name): string
{
$settings = $this->db->get_where('user_settings', ['id_users' => $user_id])->row_array();
if (empty($settings[$name])) {
throw new RuntimeException('The requested setting value was not found: ' . $user_id);
}
return $settings[$name];
}
/**
* Get the query builder interface, configured for use with the users table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('users');
}
/**
* Search users by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of settings.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$users = $this->db
->select()
->from('users')
->group_start()
->like('first_name', $keyword)
->or_like('last_name', $keyword)
->or_like('email', $keyword)
->or_like('phone_number', $keyword)
->or_like('mobile_number', $keyword)
->or_like('address', $keyword)
->or_like('city', $keyword)
->or_like('state', $keyword)
->or_like('zip_code', $keyword)
->or_like('notes', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($users as &$user) {
$this->cast($user);
$user['settings'] = $this->get_settings($user['id']);
}
return $users;
}
/**
* Get all users that match the provided criteria.
*
* @param array|string|null $where Where conditions
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of users.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$users = $this->db->get('users', $limit, $offset)->result_array();
foreach ($users as &$user) {
$this->cast($user);
$user['settings'] = $this->get_settings($user['id']);
}
return $users;
}
/**
* Load related resources to a user.
*
* @param array $user Associative array with the user data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$user, array $resources)
{
// Users do not currently have any related resources.
}
/**
* Validate the username.
*
* @param string $username Username.
* @param int|null $user_id Exclude user ID.
*
* @return bool Returns the validation result.
*/
public function validate_username(string $username, ?int $user_id = null): bool
{
if (!empty($user_id)) {
$this->db->where('id_users !=', $user_id);
}
return $this->db->get_where('user_settings', ['username' => $username])->num_rows() === 0;
}
}

View File

@@ -0,0 +1,341 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Online Appointment Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.5.0
* ---------------------------------------------------------------------------- */
/**
* Webhooks model.
*
* Handles all the database operations of the webhook resource.
*
* @package Models
*/
class Webhooks_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'is_active' => 'boolean',
'is_ssl_verified' => 'boolean',
];
/**
* @var array
*/
protected array $api_resource = [
'id' => 'id',
'name' => 'name',
'url' => 'url',
'action' => 'action',
'secretToken' => 'secret_token',
'isActive' => 'is_active',
'isSslVerified' => 'is_ssl_verified',
'notes' => 'notes',
];
/**
* Save (insert or update) a webhook.
*
* @param array $webhook Associative array with the webhook data.
*
* @return int Returns the webhook ID.
*
* @throws InvalidArgumentException
*/
public function save(array $webhook): int
{
$this->validate($webhook);
if (empty($webhook['id'])) {
return $this->insert($webhook);
} else {
return $this->update($webhook);
}
}
/**
* Validate the webhook data.
*
* @param array $webhook Associative array with the webhook data.
*
* @throws InvalidArgumentException
*/
public function validate(array $webhook): void
{
if (empty($webhook['name']) || empty($webhook['url'])) {
throw new InvalidArgumentException('Not all required fields are provided: ' . print_r($webhook, true));
}
}
/**
* Insert a new webhook into the database.
*
* @param array $webhook Associative array with the webhook data.
*
* @return int Returns the webhook ID.
*
* @throws RuntimeException
*/
protected function insert(array $webhook): int
{
$webhook['create_datetime'] = date('Y-m-d H:i:s');
$webhook['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->insert('webhooks', $webhook)) {
throw new RuntimeException('Could not insert webhook.');
}
return $this->db->insert_id();
}
/**
* Update an existing webhook.
*
* @param array $webhook Associative array with the webhook data.
*
* @return int Returns the webhook ID.
*
* @throws RuntimeException
*/
protected function update(array $webhook): int
{
$webhook['update_datetime'] = date('Y-m-d H:i:s');
if (!$this->db->update('webhooks', $webhook, ['id' => $webhook['id']])) {
throw new RuntimeException('Could not update webhook.');
}
return $webhook['id'];
}
/**
* Remove an existing webhook from the database.
*
* @param int $webhook_id Webhook ID.
*
* @throws RuntimeException
*/
public function delete(int $webhook_id): void
{
$this->db->delete('webhooks', ['id' => $webhook_id]);
}
/**
* Get a specific webhook from the database.
*
* @param int $webhook_id The ID of the record to be returned.
*
* @return array Returns an array with the webhook data.
*/
public function find(int $webhook_id): array
{
$webhook = $this->db->get_where('webhooks', ['id' => $webhook_id])->row_array();
if (!$webhook) {
throw new InvalidArgumentException('The provided webhook ID was not found in the database: ' . $webhook_id);
}
$this->cast($webhook);
return $webhook;
}
/**
* Get a specific field value from the database.
*
* @param int $webhook_id Webhook ID.
* @param string $field Name of the value to be returned.
*
* @return mixed Returns the selected webhook value from the database.
*
* @throws InvalidArgumentException
*/
public function value(int $webhook_id, string $field): mixed
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($webhook_id)) {
throw new InvalidArgumentException('The webhook ID argument cannot be empty.');
}
// Check whether the webhook exists.
$query = $this->db->get_where('webhooks', ['id' => $webhook_id]);
if (!$query->num_rows()) {
throw new InvalidArgumentException('The provided webhook ID was not found in the database: ' . $webhook_id);
}
// Check if the required field is part of the webhook data.
$webhook = $query->row_array();
$this->cast($webhook);
if (!array_key_exists($field, $webhook)) {
throw new InvalidArgumentException('The requested field was not found in the webhook data: ' . $field);
}
return $webhook[$field];
}
/**
* Get the query builder interface, configured for use with the webhooks table.
*
* @return CI_DB_query_builder
*/
public function query(): CI_DB_query_builder
{
return $this->db->from('webhooks');
}
/**
* Search webhooks by the provided keyword.
*
* @param string $keyword Search keyword.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of webhooks.
*/
public function search(string $keyword, ?int $limit = null, ?int $offset = null, ?string $order_by = null): array
{
$webhooks = $this->db
->select()
->from('webhooks')
->group_start()
->like('name', $keyword)
->or_like('url', $keyword)
->or_like('actions', $keyword)
->group_end()
->limit($limit)
->offset($offset)
->order_by($order_by)
->get()
->result_array();
foreach ($webhooks as &$webhook) {
$this->cast($webhook);
}
return $webhooks;
}
/**
* Get all webhooks that match the provided criteria.
*
* @param array|string|null $where Where conditions.
* @param int|null $limit Record limit.
* @param int|null $offset Record offset.
* @param string|null $order_by Order by.
*
* @return array Returns an array of webhooks.
*/
public function get(
array|string|null $where = null,
?int $limit = null,
?int $offset = null,
?string $order_by = null,
): array {
if ($where !== null) {
$this->db->where($where);
}
if ($order_by !== null) {
$this->db->order_by($order_by);
}
$webhooks = $this->db->get('webhooks', $limit, $offset)->result_array();
foreach ($webhooks as &$webhook) {
$this->cast($webhook);
}
return $webhooks;
}
/**
* Load related resources to a webhook.
*
* @param array $webhook Associative array with the webhook data.
* @param array $resources Resource names to be attached.
*
* @throws InvalidArgumentException
*/
public function load(array &$webhook, array $resources)
{
// Webhooks do not currently have any related resources.
}
/**
* Convert the database webhook record to the equivalent API resource.
*
* @param array $webhook Webhook data.
*/
public function api_encode(array &$webhook): void
{
$encoded_resource = [
'id' => array_key_exists('id', $webhook) ? (int) $webhook['id'] : null,
'name' => $webhook['name'],
'url' => $webhook['url'],
'actions' => $webhook['actions'],
'secret_token' => $webhook['secret_token'],
'is_ssl_verified' => $webhook['is_ssl_verified'],
'notes' => $webhook['notes'],
];
$webhook = $encoded_resource;
}
/**
* Convert the API resource to the equivalent database webhook record.
*
* @param array $webhook API resource.
* @param array|null $base Base webhook data to be overwritten with the provided values (useful for updates).
*/
public function api_decode(array &$webhook, ?array $base = null): void
{
$decoded_resource = $base ?: [];
if (array_key_exists('id', $webhook)) {
$decoded_resource['id'] = $webhook['id'];
}
if (array_key_exists('name', $webhook)) {
$decoded_resource['name'] = $webhook['name'];
}
if (array_key_exists('url', $webhook)) {
$decoded_resource['url'] = $webhook['url'];
}
if (array_key_exists('actions', $webhook)) {
$decoded_resource['actions'] = $webhook['actions'];
}
if (array_key_exists('secretToken', $webhook)) {
$decoded_resource['secret_token'] = $webhook['secretToken'];
}
if (array_key_exists('isSslVerified', $webhook)) {
$decoded_resource['is_ssl_verified'] = $webhook['isSslVerified'];
}
if (array_key_exists('notes', $webhook)) {
$decoded_resource['notes'] = $webhook['notes'];
}
$webhook = $decoded_resource;
}
}

View File

@@ -0,0 +1,10 @@
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>