Add support for custom fields on customers (#1133)

This commit is contained in:
Alex Tselegidis 2023-12-11 09:54:29 +01:00
parent aa10b57b3a
commit e6b3ffd66b
47 changed files with 769 additions and 434 deletions

View File

@ -43,6 +43,7 @@ developers to maintain and readjust their custom modifications on the main proje
- Create new layout structure for the markup, so that common HTML markup is being reused (#1152)
- Have an option to hide customer data fields during booking (#1081)
- Add a SECURITY.md file to the repository (#1122)
- Add support for custom fields on customers (#1133)
### Changed

View File

@ -18,7 +18,8 @@
*
* @package Controllers
*/
class Customers extends EA_Controller {
class Customers extends EA_Controller
{
/**
* Customers constructor.
*/
@ -49,10 +50,8 @@ class Customers extends EA_Controller {
$user_id = session('user_id');
if (cannot('view', PRIV_CUSTOMERS))
{
if ($user_id)
{
if (cannot('view', PRIV_CUSTOMERS)) {
if ($user_id) {
abort(403, 'Forbidden');
}
@ -75,8 +74,7 @@ class Customers extends EA_Controller {
$secretary_providers = [];
if ($role_slug === DB_SLUG_SECRETARY)
{
if ($role_slug === DB_SLUG_SECRETARY) {
$secretary = $this->secretaries_model->find($user_id);
$secretary_providers = $secretary['providers'];
@ -88,7 +86,7 @@ class Customers extends EA_Controller {
'date_format' => $date_format,
'time_format' => $time_format,
'timezones' => $this->timezones->to_array(),
'secretary_providers' => $secretary_providers,
'secretary_providers' => $secretary_providers
]);
html_vars([
@ -105,21 +103,45 @@ class Customers extends EA_Controller {
'require_address' => $require_address,
'require_city' => $require_city,
'require_zip_code' => $require_zip_code,
'available_languages' => config('available_languages'),
'available_languages' => config('available_languages')
]);
$this->load->view('pages/customers');
}
/**
* Find a customer.
*/
public function find()
{
try {
if (cannot('view', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
$user_id = session('user_id');
$customer_id = request('customer_id');
if (!$this->permissions->has_customer_access($user_id, $customer_id)) {
abort(403, 'Forbidden');
}
$customer = $this->customers_model->find($customer_id);
json_response($customer);
} catch (Throwable $e) {
json_exception($e);
}
}
/**
* Filter customers by the provided keyword.
*/
public function search()
{
try
{
if (cannot('view', PRIV_CUSTOMERS))
{
try {
if (cannot('view', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
@ -135,10 +157,8 @@ class Customers extends EA_Controller {
$user_id = session('user_id');
foreach ($customers as $index => &$customer)
{
if ( ! $this->permissions->has_customer_access($user_id, $customer['id']))
{
foreach ($customers as $index => &$customer) {
if (!$this->permissions->has_customer_access($user_id, $customer['id'])) {
unset($customers[$index]);
continue;
@ -146,21 +166,15 @@ class Customers extends EA_Controller {
$appointments = $this->appointments_model->get(['id_users_customer' => $customer['id']]);
foreach ($appointments as &$appointment)
{
$this->appointments_model->load($appointment, [
'service',
'provider',
]);
foreach ($appointments as &$appointment) {
$this->appointments_model->load($appointment, ['service', 'provider']);
}
$customer['appointments'] = $appointments;
}
json_response(array_values($customers));
}
catch (Throwable $e)
{
} catch (Throwable $e) {
json_exception($e);
}
}
@ -170,15 +184,12 @@ class Customers extends EA_Controller {
*/
public function store()
{
try
{
if (cannot('add', PRIV_CUSTOMERS))
{
try {
if (cannot('add', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
if (session('role_slug') !== DB_SLUG_ADMIN && setting('limit_customer_visibility'))
{
if (session('role_slug') !== DB_SLUG_ADMIN && setting('limit_customer_visibility')) {
abort(403);
}
@ -196,6 +207,11 @@ class Customers extends EA_Controller {
'notes',
'timezone',
'language',
'custom_field_1',
'custom_field_2',
'custom_field_3',
'custom_field_4',
'custom_field_5'
]);
$customer_id = $this->customers_model->save($customer);
@ -205,12 +221,10 @@ class Customers extends EA_Controller {
$this->webhooks_client->trigger(WEBHOOK_CUSTOMER_SAVE, $customer);
json_response([
'success' => TRUE,
'success' => true,
'id' => $customer_id
]);
}
catch (Throwable $e)
{
} catch (Throwable $e) {
json_exception($e);
}
}
@ -220,10 +234,8 @@ class Customers extends EA_Controller {
*/
public function update()
{
try
{
if (cannot('edit', PRIV_CUSTOMERS))
{
try {
if (cannot('edit', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
@ -231,8 +243,7 @@ class Customers extends EA_Controller {
$customer = request('customer');
if ( ! $this->permissions->has_customer_access($user_id, $customer['id']))
{
if (!$this->permissions->has_customer_access($user_id, $customer['id'])) {
abort(403, 'Forbidden');
}
@ -249,6 +260,11 @@ class Customers extends EA_Controller {
'notes',
'timezone',
'language',
'custom_field_1',
'custom_field_2',
'custom_field_3',
'custom_field_4',
'custom_field_5'
]);
$customer_id = $this->customers_model->save($customer);
@ -258,12 +274,10 @@ class Customers extends EA_Controller {
$this->webhooks_client->trigger(WEBHOOK_CUSTOMER_SAVE, $customer);
json_response([
'success' => TRUE,
'success' => true,
'id' => $customer_id
]);
}
catch (Throwable $e)
{
} catch (Throwable $e) {
json_exception($e);
}
}
@ -273,10 +287,8 @@ class Customers extends EA_Controller {
*/
public function destroy()
{
try
{
if (cannot('delete', PRIV_CUSTOMERS))
{
try {
if (cannot('delete', PRIV_CUSTOMERS)) {
abort(403, 'Forbidden');
}
@ -284,8 +296,7 @@ class Customers extends EA_Controller {
$customer_id = request('customer_id');
if ( ! $this->permissions->has_customer_access($user_id, $customer_id))
{
if (!$this->permissions->has_customer_access($user_id, $customer_id)) {
abort(403, 'Forbidden');
}
@ -296,42 +307,9 @@ class Customers extends EA_Controller {
$this->webhooks_client->trigger(WEBHOOK_CUSTOMER_DELETE, $customer);
json_response([
'success' => TRUE,
'success' => true
]);
}
catch (Throwable $e)
{
json_exception($e);
}
}
/**
* Find a customer.
*/
public function find()
{
try
{
if (cannot('view', PRIV_CUSTOMERS))
{
abort(403, 'Forbidden');
}
$user_id = session('user_id');
$customer_id = request('customer_id');
if ( ! $this->permissions->has_customer_access($user_id, $customer_id))
{
abort(403, 'Forbidden');
}
$customer = $this->customers_model->find($customer_id);
json_response($customer);
}
catch (Throwable $e)
{
} catch (Throwable $e) {
json_exception($e);
}
}

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -446,5 +446,7 @@ $lang['blocked_periods'] = 'Blocked Periods';
$lang['blocked_period_save'] = 'Blocked Period Save';
$lang['blocked_period_delete'] = 'Blocked Period Delete';
$lang['blocked_periods_hint'] = 'Define periods of time where public bookings will be disabled for all providers (e.g. closed dates, holidays etc.).';
$lang['auxiliary_field'] = 'Auxiliary Field';
$lang['custom_field'] = 'Custom Field';
$lang['custom_fields'] = 'Custom Fields';
$lang['label'] = 'Label';
// End

View File

@ -0,0 +1,56 @@
<?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.4.0
* ---------------------------------------------------------------------------- */
class Migration_Add_custom_fields_columns_to_users_table extends EA_Migration
{
/**
* @var int
*/
private const FIELD_NUMBER = 5;
/**
* Upgrade method.
*/
public function up()
{
for ($i = self::FIELD_NUMBER; $i > 0; $i--) {
$field_name = 'custom_field_' . $i;
if (!$this->db->field_exists($field_name, 'users')) {
$fields = [
$field_name => [
'type' => 'TEXT',
'null' => true,
'after' => 'language'
]
];
$this->dbforge->add_column('users', $fields);
}
}
}
/**
* Downgrade method.
*/
public function down()
{
for ($i = self::FIELD_NUMBER; $i > 0; $i--) {
$field_name = 'custom_fields_' . $i;
if ($this->db->field_exists($field_name, 'users')) {
$this->dbforge->drop_column('users', $field_name);
}
}
}
}

View File

@ -0,0 +1,65 @@
<?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.4.0
* ---------------------------------------------------------------------------- */
class Migration_Insert_custom_field_rows_to_settings_table extends EA_Migration
{
/**
* @var int
*/
private const FIELD_NUMBER = 5;
private const SETTINGS = [
'display' => '0',
'require' => '0',
'label' => ''
];
/**
* Upgrade method.
*/
public function up()
{
for ($i = 1; $i <= self::FIELD_NUMBER; $i++) {
$field_name = 'custom_field_' . $i;
foreach (self::SETTINGS as $name => $default_value) {
$setting_name = $name . '_' . $field_name;
if (!$this->db->get_where('settings', ['name' => $setting_name])->num_rows()) {
$this->db->insert('settings', [
'name' => $setting_name,
'value' => $default_value
]);
}
}
}
}
/**
* Downgrade method.
*/
public function down()
{
for ($i = 1; $i >= self::FIELD_NUMBER; $i++) {
$field_name = 'custom_field_' . $i;
foreach (self::SETTINGS as $name => $default_value) {
$setting_name = $name . '_' . $field_name;
if ($this->db->get_where('settings', ['name' => $setting_name])->num_rows()) {
$this->db->delete('settings', ['name' => $setting_name]);
}
}
}
}
}

View File

@ -18,13 +18,14 @@
*
* @package Models
*/
class Customers_model extends EA_Model {
class Customers_model extends EA_Model
{
/**
* @var array
*/
protected array $casts = [
'id' => 'integer',
'id_roles' => 'integer',
'id_roles' => 'integer'
];
/**
@ -42,7 +43,12 @@ class Customers_model extends EA_Model {
'zip' => 'zip_code',
'timezone' => 'timezone',
'language' => 'language',
'notes' => 'notes',
'customField1' => 'custom_field_1',
'customField2' => 'custom_field_2',
'customField3' => 'custom_field_3',
'customField4' => 'custom_field_4',
'customField5' => 'custom_field_5',
'notes' => 'notes'
];
/**
@ -58,17 +64,13 @@ class Customers_model extends EA_Model {
{
$this->validate($customer);
if ($this->exists($customer) && empty($customer['id']))
{
if ($this->exists($customer) && empty($customer['id'])) {
$customer['id'] = $this->find_record_id($customer);
}
if (empty($customer['id']))
{
if (empty($customer['id'])) {
return $this->insert($customer);
}
else
{
} else {
return $this->update($customer);
}
}
@ -83,13 +85,13 @@ class Customers_model extends EA_Model {
public function validate(array $customer)
{
// If a customer ID is provided then check whether the record really exists in the database.
if ( ! empty($customer['id']))
{
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']);
if (!$count) {
throw new InvalidArgumentException(
'The provided customer ID does not exist in the database: ' . $customer['id']
);
}
}
@ -103,31 +105,27 @@ class Customers_model extends EA_Model {
$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));
(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']))
{
if (!empty($customer['email'])) {
// Validate the email address.
if ( ! filter_var($customer['email'], FILTER_VALIDATE_EMAIL))
{
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;
$customer_id = $customer['id'] ?? null;
$count = $this
->db
$count = $this->db
->select()
->from('users')
->join('roles', 'roles.id = users.id_roles', 'inner')
@ -137,13 +135,123 @@ class Customers_model extends EA_Model {
->get()
->num_rows();
if ($count > 0)
{
throw new InvalidArgumentException('The provided email address is already in use, please use a different one.');
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 $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.
*
@ -159,8 +267,7 @@ class Customers_model extends EA_Model {
$customer['update_datetime'] = date('Y-m-d H:i:s');
$customer['id_roles'] = $this->get_customer_role_id();
if ( ! $this->db->insert('users', $customer))
{
if (!$this->db->insert('users', $customer)) {
throw new RuntimeException('Could not insert customer.');
}
@ -180,8 +287,7 @@ class Customers_model extends EA_Model {
{
$customer['update_datetime'] = date('Y-m-d H:i:s');
if ( ! $this->db->update('users', $customer, ['id' => $customer['id']]))
{
if (!$this->db->update('users', $customer, ['id' => $customer['id']])) {
throw new RuntimeException('Could not update customer.');
}
@ -211,9 +317,10 @@ class Customers_model extends EA_Model {
{
$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);
if (!$customer) {
throw new InvalidArgumentException(
'The provided customer ID was not found in the database: ' . $customer_id
);
}
$this->cast($customer);
@ -233,22 +340,21 @@ class Customers_model extends EA_Model {
*/
public function value(int $customer_id, string $field): mixed
{
if (empty($field))
{
if (empty($field)) {
throw new InvalidArgumentException('The field argument is cannot be empty.');
}
if (empty($customer_id))
{
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);
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.
@ -256,128 +362,13 @@ class Customers_model extends EA_Model {
$this->cast($customer);
if ( ! array_key_exists($field, $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 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 $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'];
}
/**
* Get the query builder interface, configured for use with the users (customer-filtered) table.
*
@ -400,12 +391,11 @@ class Customers_model extends EA_Model {
*
* @return array Returns an array of customers.
*/
public function search(string $keyword, int $limit = NULL, int $offset = NULL, string $order_by = NULL): array
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
$customers = $this->db
->select()
->from('users')
->where('id_roles', $role_id)
@ -428,8 +418,7 @@ class Customers_model extends EA_Model {
->get()
->result_array();
foreach ($customers as &$customer)
{
foreach ($customers as &$customer) {
$this->cast($customer);
}
@ -457,7 +446,7 @@ class Customers_model extends EA_Model {
public function api_encode(array &$customer)
{
$encoded_resource = [
'id' => array_key_exists('id', $customer) ? (int)$customer['id'] : NULL,
'id' => array_key_exists('id', $customer) ? (int) $customer['id'] : null,
'firstName' => $customer['first_name'],
'lastName' => $customer['last_name'],
'email' => $customer['email'],
@ -467,6 +456,11 @@ class Customers_model extends EA_Model {
'zip' => $customer['zip_code'],
'notes' => $customer['notes'],
'timezone' => $customer['timezone'],
'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']
];
$customer = $encoded_resource;
@ -478,52 +472,67 @@ class Customers_model extends EA_Model {
* @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)
public function api_decode(array &$customer, array $base = null)
{
$decoded_resource = $base ?: [];
if (array_key_exists('id', $customer))
{
if (array_key_exists('id', $customer)) {
$decoded_resource['id'] = $customer['id'];
}
if (array_key_exists('firstName', $customer))
{
if (array_key_exists('firstName', $customer)) {
$decoded_resource['first_name'] = $customer['firstName'];
}
if (array_key_exists('lastName', $customer))
{
if (array_key_exists('lastName', $customer)) {
$decoded_resource['last_name'] = $customer['lastName'];
}
if (array_key_exists('email', $customer))
{
if (array_key_exists('email', $customer)) {
$decoded_resource['email'] = $customer['email'];
}
if (array_key_exists('phone', $customer))
{
if (array_key_exists('phone', $customer)) {
$decoded_resource['phone_number'] = $customer['phone'];
}
if (array_key_exists('address', $customer))
{
if (array_key_exists('address', $customer)) {
$decoded_resource['address'] = $customer['address'];
}
if (array_key_exists('city', $customer))
{
if (array_key_exists('city', $customer)) {
$decoded_resource['city'] = $customer['city'];
}
if (array_key_exists('zip', $customer))
{
if (array_key_exists('zip', $customer)) {
$decoded_resource['zip_code'] = $customer['zip'];
}
if (array_key_exists('notes', $customer))
{
if (array_key_exists('language', $customer)) {
$decoded_resource['language'] = $customer['language'];
}
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('notes', $customer)) {
$decoded_resource['notes'] = $customer['notes'];
}

View File

@ -43,27 +43,21 @@
<?php
// Group services by category, only if there is at least one service
// with a parent category.
$has_category = FALSE;
$has_category = false;
foreach ($available_services as $service)
{
if ( ! empty($service['category_id']))
{
$has_category = TRUE;
foreach ($available_services as $service) {
if (!empty($service['category_id'])) {
$has_category = true;
break;
}
}
if ($has_category)
{
if ($has_category) {
$grouped_services = [];
foreach ($available_services as $service)
{
if ( ! empty($service['category_id']))
{
if ( ! isset($grouped_services[$service['category_name']]))
{
foreach ($available_services as $service) {
if (!empty($service['category_id'])) {
if (!isset($grouped_services[$service['category_name']])) {
$grouped_services[$service['category_name']] = [];
}
@ -75,40 +69,39 @@
// another iteration only for the uncategorized services.
$grouped_services['uncategorized'] = [];
foreach ($available_services as $service)
{
if ($service['category_id'] == NULL)
{
foreach ($available_services as $service) {
if ($service['category_id'] == null) {
$grouped_services['uncategorized'][] = $service;
}
}
foreach ($grouped_services as $key => $group)
{
$group_label = $key !== 'uncategorized'
? e($group[0]['category_name'])
: 'Uncategorized';
foreach ($grouped_services as $key => $group) {
$group_label =
$key !== 'uncategorized'
? e($group[0]['category_name'])
: 'Uncategorized';
if (count($group) > 0)
{
if (count($group) > 0) {
echo '<optgroup label="' . $group_label . '">';
foreach ($group as $service)
{
echo '<option value="' . $service['id'] . '">'
. e($service['name']) . '</option>';
foreach ($group as $service) {
echo '<option value="' .
$service['id'] .
'">' .
e($service['name']) .
'</option>';
}
echo '</optgroup>';
}
}
}
else
{
foreach ($available_services as $service)
{
echo '<option value="' . $service['id'] . '">'
. e($service['name']) . '</option>';
} else {
foreach ($available_services as $service) {
echo '<option value="' .
$service['id'] .
'">' .
e($service['name']) .
'</option>';
}
}
?>
@ -124,7 +117,7 @@
</div>
<div class="mb-3">
<?php component('color_selection', ['attributes' => 'id="appointment-color"']) ?>
<?php component('color_selection', ['attributes' => 'id="appointment-color"']); ?>
</div>
<div class="mb-3">
@ -143,7 +136,7 @@
<option value="<?= e($appointment_status_option) ?>">
<?= e($appointment_status_option) ?>
</option>
<?php endforeach ?>
<?php endforeach; ?>
</select>
</div>
</div>
@ -228,7 +221,7 @@
<?= lang('first_name') ?>
<?php if ($require_first_name): ?>
<span class="text-danger">*</span>
<?php endif ?>
<?php endif; ?>
</label>
<input type="text" id="first-name"
class="<?= $require_first_name ? 'required' : '' ?> form-control"
@ -240,7 +233,7 @@
<?= lang('last_name') ?>
<?php if ($require_last_name): ?>
<span class="text-danger">*</span>
<?php endif ?>
<?php endif; ?>
</label>
<input type="text" id="last-name"
class="<?= $require_last_name ? 'required' : '' ?> form-control"
@ -252,7 +245,7 @@
<?= lang('email') ?>
<?php if ($require_email): ?>
<span class="text-danger">*</span>
<?php endif ?>
<?php endif; ?>
</label>
<input type="text" id="email"
class="<?= $require_email ? 'required' : '' ?> form-control"
@ -264,7 +257,7 @@
<?= lang('phone_number') ?>
<?php if ($require_phone_number): ?>
<span class="text-danger">*</span>
<?php endif ?>
<?php endif; ?>
</label>
<input type="text" id="phone-number" maxlength="60"
class="<?= $require_phone_number ? 'required' : '' ?> form-control"/>
@ -280,7 +273,7 @@
<option value="<?= $available_language ?>">
<?= ucfirst($available_language) ?>
</option>
<?php endforeach ?>
<?php endforeach; ?>
</select>
</div>
</div>
@ -290,7 +283,7 @@
<?= lang('address') ?>
<?php if ($require_address): ?>
<span class="text-danger">*</span>
<?php endif ?>
<?php endif; ?>
</label>
<input type="text" id="address"
class="<?= $require_address ? 'required' : '' ?> form-control"
@ -302,7 +295,7 @@
<?= lang('city') ?>
<?php if ($require_city): ?>
<span class="text-danger">*</span>
<?php endif ?>
<?php endif; ?>
</label>
<input type="text" id="city"
class="<?= $require_city ? 'required' : '' ?> form-control"
@ -314,7 +307,7 @@
<?= lang('zip_code') ?>
<?php if ($require_zip_code): ?>
<span class="text-danger">*</span>
<?php endif ?>
<?php endif; ?>
</label>
<input type="text" id="zip-code"
class="<?= $require_zip_code ? 'required' : '' ?> form-control"
@ -329,7 +322,7 @@
<?php component('timezone_dropdown', [
'attributes' => 'id="timezone" class="form-control required"',
'grouped_timezones' => vars('grouped_timezones')
]) ?>
]); ?>
</div>
<div class="mb-3">
@ -337,12 +330,16 @@
<?= lang('notes') ?>
<?php if ($require_notes): ?>
<span class="text-danger">*</span>
<?php endif ?>
<?php endif; ?>
</label>
<textarea id="customer-notes" rows="2"
class="<?= $require_notes ? 'required' : '' ?> form-control"></textarea>
</div>
</div>
<div class="mb-3">
<?php component('custom_fields'); ?>
</div>
</div>
</fieldset>
</form>
@ -361,8 +358,8 @@
</div>
</div>
<?php section('scripts') ?>
<?php section('scripts'); ?>
<script src="<?= asset_url('assets/js/components/appointments_modal.js') ?>"></script>
<?php end_section('scripts') ?>
<?php end_section('scripts'); ?>

View File

@ -131,6 +131,10 @@
</div>
<?php endif; ?>
</div>
<div class="mb-3">
<?php component('custom_fields'); ?>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
<?php
/**
* Local variables.
*
* @var bool $disabled (false)
*/
$disabled = $disabled ?? false; ?>
<?php for ($i = 1; $i <= 5; $i++): ?>
<?php if (setting('display_custom_field_' . $i)): ?>
<div class="mb-3">
<label for="custom-field-<?= $i ?>" class="form-label">
<?= setting('label_custom_field_' . $i) ?: lang('custom_field') . ' #' . $i ?>
<?php if (vars('require_custom_field_' . $i)): ?>
<span class="text-danger" hidden>*</span>
<?php endif; ?>
</label>
<input type="text" id="custom-field-<?= $i ?>"
class="<?= vars('require_custom_field_' . $i) ? 'required' : '' ?> form-control"
maxlength="120" <?= $disabled ? 'disabled' : '' ?>/>
</div>
<?php endif; ?>
<?php endfor; ?>

View File

@ -246,6 +246,50 @@
</div>
</div>
<h5 class="text-black-50 mb-3 fw-light">
<?= lang('custom_fields') ?>
</h5>
<div class="row mb-5 fields-row">
<?php for ($i = 1; $i <= 5; $i++): ?>
<div class="col-sm-6">
<div class="form-group mb-5">
<label for="first-name" class="form-label">
<?= lang('custom_field') ?> #<?= $i ?>
<span class="text-danger">*</span>
</label>
<input type="text" id="custom-field-<?= $i ?>" class="form-control mb-2"
placeholder="<?= lang('label') ?>"
data-field="label_custom_field_<?= $i ?>"
aria-label="label"
/>
<div class="d-flex">
<div class="form-check form-switch me-4">
<input class="form-check-input display-switch" type="checkbox"
id="display-custom-field-<?= $i ?>"
data-field="display_custom_field_<?= $i ?>">
<label class="form-check-label" for="display-custom-field-<?= $i ?>">
<?= lang('display') ?>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input require-switch" type="checkbox"
id="require-custom-field-<?= $i ?>"
data-field="require_custom_field_<?= $i ?>">
<label class="form-check-label" for="require-custom-field-<?= $i ?>">
<?= lang('require') ?>
</label>
</div>
</div>
</div>
</div>
<?php endfor; ?>
</div>
<h5 class="text-black-50 mb-3 fw-light">
<?= lang('options') ?>
</h5>

View File

@ -181,7 +181,9 @@
]); ?>
</div>
<?php component('auxiliary_fields'); ?>
<?php component('custom_fields', [
'disabled' => true
]); ?>
<div class="mb-3">
<label class="form-label" for="notes">

View File

@ -46,6 +46,11 @@ App.Components.AppointmentsModal = (function () {
const $insertAppointment = $('#insert-appointment');
const $existingCustomersList = $('#existing-customers-list');
const $newCustomer = $('#new-customer');
const $customField1 = $('#custom-field-1');
const $customField2 = $('#custom-field-2');
const $customField3 = $('#custom-field-3');
const $customField4 = $('#custom-field-4');
const $customField5 = $('#custom-field-5');
/**
* Update the displayed timezone.
@ -110,7 +115,12 @@ App.Components.AppointmentsModal = (function () {
zip_code: $zipCode.val(),
language: $language.val(),
timezone: $timezone.val(),
notes: $customerNotes.val()
notes: $customerNotes.val(),
custom_field_1: $customField1.val(),
custom_field_2: $customField2.val(),
custom_field_3: $customField3.val(),
custom_field_4: $customField4.val(),
custom_field_5: $customField5.val()
};
if ($customerId.val() !== '') {
@ -196,7 +206,7 @@ App.Components.AppointmentsModal = (function () {
$startDatetime[0]._flatpickr.setDate(startMoment.toDate());
$endDatetime[0]._flatpickr.setDate(startMoment.add(duration, 'minutes').toDate());
// Display modal form.
$appointmentsModal.find('.modal-header h3').text(lang('new_appointment_title'));
@ -217,7 +227,8 @@ App.Components.AppointmentsModal = (function () {
vars('customers').forEach((customer) => {
$('<div/>', {
'data-id': customer.id,
'text': (customer.first_name || '[No First Name]') + ' ' + (customer.last_name || '[No Last Name]')
'text':
(customer.first_name || '[No First Name]') + ' ' + (customer.last_name || '[No Last Name]')
}).appendTo($existingCustomersList);
});
} else {
@ -249,6 +260,11 @@ App.Components.AppointmentsModal = (function () {
$language.val(customer.language);
$timezone.val(customer.timezone);
$customerNotes.val(customer.notes);
$customField1.val(customer.custom_field_1);
$customField2.val(customer.custom_field_2);
$customField3.val(customer.custom_field_3);
$customField4.val(customer.custom_field_4);
$customField5.val(customer.custom_field_5);
}
$selectCustomer.trigger('click'); // Hide the list.
@ -278,7 +294,10 @@ App.Components.AppointmentsModal = (function () {
response.forEach((customer) => {
$('<div/>', {
'data-id': customer.id,
'text': (customer.first_name || '[No First Name]') + ' ' + (customer.last_name || '[No Last Name]')
'text':
(customer.first_name || '[No First Name]') +
' ' +
(customer.last_name || '[No Last Name]')
}).appendTo($existingCustomersList);
// Verify if this customer is on the old customer list.
@ -309,7 +328,10 @@ App.Components.AppointmentsModal = (function () {
) {
$('<div/>', {
'data-id': customer.id,
'text': (customer.first_name || '[No First Name]') + ' ' + (customer.last_name || '[No Last Name]')
'text':
(customer.first_name || '[No First Name]') +
' ' +
(customer.last_name || '[No Last Name]')
}).appendTo($existingCustomersList);
}
});
@ -328,8 +350,8 @@ App.Components.AppointmentsModal = (function () {
*/
$selectService.on('change', () => {
const serviceId = $selectService.val();
const providerId = $selectProvider.val();
const providerId = $selectProvider.val();
$selectProvider.empty();
@ -341,7 +363,7 @@ App.Components.AppointmentsModal = (function () {
const duration = service ? service.duration : 60;
const start = $startDatetime[0]._flatpickr.selectedDates[0];
$endDatetime[0]._flatpickr.setDate( new Date(start.getTime() + duration * 60000));
$endDatetime[0]._flatpickr.setDate(new Date(start.getTime() + duration * 60000));
// Update the providers select box.
@ -366,7 +388,7 @@ App.Components.AppointmentsModal = (function () {
$selectProvider.append(new Option(provider.first_name + ' ' + provider.last_name, provider.id));
}
});
if ($selectProvider.find(`option[value="${providerId}"]`).length) {
$selectProvider.val(providerId);
}
@ -395,6 +417,11 @@ App.Components.AppointmentsModal = (function () {
$language.val('english');
$timezone.val('UTC');
$customerNotes.val('');
$customField1.val('');
$customField2.val('');
$customField3.val('');
$customField4.val('');
$customField5.val('');
});
}
@ -464,14 +491,14 @@ App.Components.AppointmentsModal = (function () {
);
const start = $startDatetime[0]._flatpickr.selectedDates[0];
$endDatetime[0]._flatpickr.setDate( new Date(start.getTime() + service.duration * 60000));
$endDatetime[0]._flatpickr.setDate(new Date(start.getTime() + service.duration * 60000));
}
});
$startDatetime[0]._flatpickr.setDate( startDatetime);
$startDatetime[0]._flatpickr.setDate(startDatetime);
App.Utils.UI.initializeDatetimepicker($endDatetime);
$endDatetime[0]._flatpickr.setDate( endDatetime);
$endDatetime[0]._flatpickr.setDate(endDatetime);
}
/**
@ -502,7 +529,10 @@ App.Components.AppointmentsModal = (function () {
}
// Check email address.
if ($appointmentsModal.find('#email').val() && !App.Utils.Validation.email($appointmentsModal.find('#email').val())) {
if (
$appointmentsModal.find('#email').val() &&
!App.Utils.Validation.email($appointmentsModal.find('#email').val())
) {
$appointmentsModal.find('#email').addClass('is-invalid');
throw new Error(lang('invalid_email'));
}

View File

@ -27,9 +27,15 @@ App.Pages.Customers = (function () {
const $zipCode = $('#zip-code');
const $timezone = $('#timezone');
const $language = $('#language');
const $customField1 = $('#custom-field-1');
const $customField2 = $('#custom-field-2');
const $customField3 = $('#custom-field-3');
const $customField4 = $('#custom-field-4');
const $customField5 = $('#custom-field-5');
const $notes = $('#notes');
const $formMessage = $('#form-message');
const $customerAppointments = $('#customer-appointments');
let filterResults = {};
let filterLimit = 20;
@ -124,7 +130,12 @@ App.Pages.Customers = (function () {
zip_code: $zipCode.val(),
notes: $notes.val(),
timezone: $timezone.val(),
language: $language.val() || 'english'
language: $language.val() || 'english',
custom_field_1: $customField1.val(),
custom_field_2: $customField2.val(),
custom_field_3: $customField3.val(),
custom_field_4: $customField4.val(),
custom_field_5: $customField5.val()
};
if ($id.val()) {
@ -276,6 +287,11 @@ App.Pages.Customers = (function () {
$notes.val(customer.notes);
$timezone.val(customer.timezone);
$language.val(customer.language || 'english');
$customField1.val(customer.custom_field_1);
$customField2.val(customer.custom_field_2);
$customField3.val(customer.custom_field_3);
$customField4.val(customer.custom_field_4);
$customField5.val(customer.custom_field_5);
$customerAppointments.empty();

View File

@ -158,6 +158,11 @@ App.Utils.CalendarDefaultView = (function () {
$appointmentsModal.find('#appointment-status').val(appointment.status);
$appointmentsModal.find('#appointment-notes').val(appointment.notes);
$appointmentsModal.find('#customer-notes').val(customer.notes);
$appointmentsModal.find('#custom-field-1').val(customer.custom_field_1);
$appointmentsModal.find('#custom-field-2').val(customer.custom_field_2);
$appointmentsModal.find('#custom-field-3').val(customer.custom_field_3);
$appointmentsModal.find('#custom-field-4').val(customer.custom_field_4);
$appointmentsModal.find('#custom-field-5').val(customer.custom_field_5);
App.Components.ColorSelection.setColor(
$appointmentsModal.find('#appointment-color'),
@ -287,7 +292,10 @@ App.Utils.CalendarDefaultView = (function () {
*/
$selectFilterItem.on('change', () => {
// If current value is service, then the sync buttons must be disabled.
if ($selectFilterItem.find('option:selected').attr('type') === FILTER_TYPE_SERVICE || $selectFilterItem.val() === 'all') {
if (
$selectFilterItem.find('option:selected').attr('type') === FILTER_TYPE_SERVICE ||
$selectFilterItem.val() === 'all'
) {
$('#google-sync, #enable-sync, #insert-appointment, #insert-dropdown').prop('disabled', true);
fullCalendar.setOption('selectable', false);
fullCalendar.setOption('editable', false);
@ -317,7 +325,9 @@ App.Utils.CalendarDefaultView = (function () {
$('#google-sync').prop('disabled', true);
}
$('#insert-working-plan-exception').toggle(providerId !== App.Utils.CalendarDefaultView.FILTER_TYPE_ALL);
$('#insert-working-plan-exception').toggle(
providerId !== App.Utils.CalendarDefaultView.FILTER_TYPE_ALL
);
}
$reloadAppointments.trigger('click');
@ -382,10 +392,10 @@ App.Utils.CalendarDefaultView = (function () {
$target.hasClass('fc-custom') && vars('privileges').appointments.edit === true ? '' : 'd-none';
displayDelete =
$target.hasClass('fc-custom') && vars('privileges').appointments.delete === true ? 'me-2' : 'd-none'; // Same value at the time.
let startDateTimeObject = info.event.start;
let endDateTimeObject = info.event.end || info.event.start;
if (info.event.extendedProps.data) {
startDateTimeObject = new Date(info.event.extendedProps.data.start_datetime);
endDateTimeObject = new Date(info.event.extendedProps.data.end_datetime);
@ -496,11 +506,14 @@ App.Utils.CalendarDefaultView = (function () {
'text': lang('start')
}),
$('<span/>', {
'text': startTime ? App.Utils.Date.format(`${date} ${startTime}`,
vars('date_format'),
vars('time_format'),
true
) : '-'
'text': startTime
? App.Utils.Date.format(
`${date} ${startTime}`,
vars('date_format'),
vars('time_format'),
true
)
: '-'
}),
$('<br/>'),
@ -509,11 +522,14 @@ App.Utils.CalendarDefaultView = (function () {
'text': lang('end')
}),
$('<span/>', {
'text': endTime ? App.Utils.Date.format(`${date} ${endTime}`,
vars('date_format'),
vars('time_format'),
true
) : '-'
'text': endTime
? App.Utils.Date.format(
`${date} ${endTime}`,
vars('date_format'),
vars('time_format'),
true
)
: '-'
}),
$('<br/>'),
@ -626,7 +642,7 @@ App.Utils.CalendarDefaultView = (function () {
'text': lang('status')
}),
$('<span/>', {
'text': info.event.extendedProps.data.status || '-',
'text': info.event.extendedProps.data.status || '-'
}),
$('<br/>'),
@ -1025,8 +1041,7 @@ App.Utils.CalendarDefaultView = (function () {
return;
}
const isProviderDisplayed =
$selectFilterItem.find('option:selected').attr('type') === FILTER_TYPE_PROVIDER;
const isProviderDisplayed = $selectFilterItem.find('option:selected').attr('type') === FILTER_TYPE_PROVIDER;
const buttons = [
{
@ -1059,8 +1074,7 @@ App.Utils.CalendarDefaultView = (function () {
if (isProviderDisplayed) {
const provider = vars('available_providers').find(
(availableProvider) =>
Number(availableProvider.id) === Number($selectFilterItem.val())
(availableProvider) => Number(availableProvider.id) === Number($selectFilterItem.val())
);
if (provider) {
@ -1090,8 +1104,7 @@ App.Utils.CalendarDefaultView = (function () {
$appointmentsModal.find('#select-provider').trigger('change');
} else {
service = vars('available_services').find(
(availableService) =>
Number(availableService.id) === Number($selectFilterItem.val())
(availableService) => Number(availableService.id) === Number($selectFilterItem.val())
);
if (service) {
@ -1182,9 +1195,7 @@ App.Utils.CalendarDefaultView = (function () {
// Add appointments to calendar.
response.appointments.forEach((appointment) => {
const title = [
appointment.service.name
];
const title = [appointment.service.name];
const customerInfo = [];
@ -1236,7 +1247,7 @@ App.Utils.CalendarDefaultView = (function () {
calendarEventSource.push(unavailabilityEvent);
});
response.blocked_periods.forEach(blockedPeriod => {
response.blocked_periods.forEach((blockedPeriod) => {
const blockedPeriodEvent = {
title: blockedPeriod.name,
start: moment(blockedPeriod.start_datetime).toDate(),
@ -1263,7 +1274,9 @@ App.Utils.CalendarDefaultView = (function () {
(availableProvider) => Number(availableProvider.id) === Number(recordId)
);
const workingPlan = JSON.parse(provider ? provider.settings.working_plan : vars('company_working_plan'));
const workingPlan = JSON.parse(
provider ? provider.settings.working_plan : vars('company_working_plan')
);
const workingPlanExceptions = JSON.parse(provider ? provider.settings.working_plan_exceptions : '{}');
let unavailabilityEvent;
let breakStart;
@ -1304,9 +1317,7 @@ App.Utils.CalendarDefaultView = (function () {
workingPlanExceptionEvent = {
title: lang('working_plan_exception'),
start: moment(workingPlanExceptionStart, 'YYYY-MM-DD HH:mm', true).toDate(),
end: moment(workingPlanExceptionEnd, 'YYYY-MM-DD HH:mm', true)
.add(1, 'day')
.toDate(),
end: moment(workingPlanExceptionEnd, 'YYYY-MM-DD HH:mm', true).add(1, 'day').toDate(),
allDay: true,
color: '#879DB4',
editable: false,
@ -1352,10 +1363,7 @@ App.Utils.CalendarDefaultView = (function () {
title: lang('not_working'),
start: calendarDate.clone().toDate(),
end: moment(
calendarDate.format('YYYY-MM-DD') +
' ' +
sortedWorkingPlan[weekdayName].start +
':00'
calendarDate.format('YYYY-MM-DD') + ' ' + sortedWorkingPlan[weekdayName].start + ':00'
).toDate(),
allDay: false,
color: '#BEBEBE',
@ -1376,10 +1384,7 @@ App.Utils.CalendarDefaultView = (function () {
unavailabilityEvent = {
title: lang('not_working'),
start: moment(
calendarDate.format('YYYY-MM-DD') +
' ' +
sortedWorkingPlan[weekdayName].end +
':00'
calendarDate.format('YYYY-MM-DD') + ' ' + sortedWorkingPlan[weekdayName].end + ':00'
).toDate(),
end: calendarDate.clone().add(1, 'day').toDate(),
allDay: false,
@ -1405,9 +1410,7 @@ App.Utils.CalendarDefaultView = (function () {
const unavailabilityEvent = {
title: lang('break'),
start: moment(
calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.start
).toDate(),
start: moment(calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.start).toDate(),
end: moment(calendarDate.format('YYYY-MM-DD') + ' ' + breakPeriod.end).toDate(),
allDay: false,
color: '#BEBEBE',
@ -1420,7 +1423,6 @@ App.Utils.CalendarDefaultView = (function () {
calendarDate.add(1, 'day');
}
})
.always(() => {
$('#loading').css('visibility', '');
@ -1562,7 +1564,10 @@ App.Utils.CalendarDefaultView = (function () {
const localSelectFilterItemValue = window.localStorage.getItem('EasyAppointments.SelectFilterItem');
if (localSelectFilterItemValue && $selectFilterItem.find(`option[value="${localSelectFilterItemValue}"]`).length) {
if (
localSelectFilterItemValue &&
$selectFilterItem.find(`option[value="${localSelectFilterItemValue}"]`).length
) {
$selectFilterItem.val(localSelectFilterItemValue).trigger('change');
} else {
$reloadAppointments.trigger('click');
@ -1601,6 +1606,11 @@ App.Utils.CalendarDefaultView = (function () {
$appointmentsModal.find('#appointment-status').val(appointment.status);
$appointmentsModal.find('#appointment-notes').val(appointment.notes);
$appointmentsModal.find('#customer-notes').val(customer.notes);
$appointmentsModal.find('#custom-field-1').val(customer.custom_field_1);
$appointmentsModal.find('#custom-field-2').val(customer.custom_field_2);
$appointmentsModal.find('#custom-field-3').val(customer.custom_field_3);
$appointmentsModal.find('#custom-field-4').val(customer.custom_field_4);
$appointmentsModal.find('#custom-field-5').val(customer.custom_field_5);
App.Components.ColorSelection.setColor($appointmentsModal.find('#appointment-color'), appointment.color);

View File

@ -116,7 +116,7 @@ App.Utils.CalendarTableView = (function () {
createUnavailabilities($providerColumn, response.unavailabilities);
// Add the blocked periods to the column.
createBlockedPeriods($providerColumn, response.blocked_periods);
createBlockedPeriods($providerColumn, response.blocked_periods);
// Add the provider breaks to the column.
const workingPlan = JSON.parse(provider.settings.working_plan);
@ -232,6 +232,11 @@ App.Utils.CalendarTableView = (function () {
$appointmentsModal.find('#appointment-status').val(appointment.status);
$appointmentsModal.find('#appointment-notes').val(appointment.notes);
$appointmentsModal.find('#customer-notes').val(customer.notes);
$appointmentsModal.find('#custom-field-1').val(customer.custom_field_1);
$appointmentsModal.find('#custom-field-2').val(customer.custom_field_2);
$appointmentsModal.find('#custom-field-3').val(customer.custom_field_3);
$appointmentsModal.find('#custom-field-4').val(customer.custom_field_4);
$appointmentsModal.find('#custom-field-5').val(customer.custom_field_5);
App.Components.ColorSelection.setColor(
$appointmentsModal.find('#appointment-color'),
@ -387,7 +392,9 @@ App.Utils.CalendarTableView = (function () {
App.Utils.UI.initializeDatepicker($calendarHeader.find('.select-date'), {
onChange(selectedDates) {
const startDate = selectedDates[0];
const endDate = moment(startDate).add(parseInt($selectFilterItem.val()) - 1, 'days').toDate();
const endDate = moment(startDate)
.add(parseInt($selectFilterItem.val()) - 1, 'days')
.toDate();
createView(startDate, endDate);
}
});
@ -853,7 +860,7 @@ App.Utils.CalendarTableView = (function () {
return;
}
const filterServiceIds = $filterService.val().map(serviceId => Number(serviceId));
const filterServiceIds = $filterService.val().map((serviceId) => Number(serviceId));
appointments = appointments.filter(
(appointment) => !filterServiceIds.length || filterServiceIds.includes(appointment.id_services)
@ -868,9 +875,7 @@ App.Utils.CalendarTableView = (function () {
continue;
}
const title = [
appointment.service.name
];
const title = [appointment.service.name];
const customerInfo = [];
@ -938,7 +943,7 @@ App.Utils.CalendarTableView = (function () {
$providerColumn.find('.calendar-wrapper').data('fullCalendar').addEventSource(calendarEventSource);
}
/**
* Create Blocked Period Events
*
@ -1015,11 +1020,11 @@ App.Utils.CalendarTableView = (function () {
$event.html(
lang('break') +
' <span class="hour">' +
moment(eventDate).format('HH:mm') +
'</span> (' +
eventDuration +
"')"
' <span class="hour">' +
moment(eventDate).format('HH:mm') +
'</span> (' +
eventDuration +
"')"
);
$event.data(entry);
@ -1199,8 +1204,8 @@ App.Utils.CalendarTableView = (function () {
$('<span/>', {
'text': info.event.extendedProps.data
? info.event.extendedProps.data.provider.first_name +
' ' +
info.event.extendedProps.data.provider.last_name
' ' +
info.event.extendedProps.data.provider.last_name
: '-'
}),
$('<br/>'),
@ -1212,8 +1217,8 @@ App.Utils.CalendarTableView = (function () {
$('<span/>', {
'text': App.Utils.Date.format(
info.event.extendedProps.data.date +
' ' +
info.event.extendedProps.data.workingPlanException.start,
' ' +
info.event.extendedProps.data.workingPlanException.start,
vars('date_format'),
vars('time_format'),
true
@ -1228,8 +1233,8 @@ App.Utils.CalendarTableView = (function () {
$('<span/>', {
'text': App.Utils.Date.format(
info.event.extendedProps.data.date +
' ' +
info.event.extendedProps.data.workingPlanException.end,
' ' +
info.event.extendedProps.data.workingPlanException.end,
vars('date_format'),
vars('time_format'),
true

View File

@ -1902,6 +1902,16 @@ components:
type: string
language:
type: string
customField1:
type: string
customField2:
type: string
customField3:
type: string
customField4:
type: string
customField5:
type: string
notes:
type: string
example:
@ -1914,6 +1924,11 @@ components:
zip: '12345'
timezone: UTC
language: english
customField1: Value1
customField2: Value2
customField3: Value3
customField4: Value4
customField5: Value5
notes: This is a test customer.
CustomerPayload:
type: object
@ -1936,6 +1951,16 @@ components:
type: string
language:
type: string
customField1:
type: string
customField2:
type: string
customField3:
type: string
customField4:
type: string
customField5:
type: string
notes:
type: string
example:
@ -1947,6 +1972,11 @@ components:
zip: '12345'
timezone: UTC
language: english
customField1: Value1
customField2: Value2
customField3: Value3
customField4: Value4
customField5: Value5
notes: This is a test customer.
CustomerCollection:
type: array