Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/ameliabooking/ameliabooking.php
+++ b/ameliabooking/ameliabooking.php
@@ -3,7 +3,7 @@
Plugin Name: Amelia
Plugin URI: https://wpamelia.com/
Description: Amelia is a simple yet powerful automated booking specialist, working 24/7 to make sure your customers can make appointments and events even while you sleep!
-Version: 2.3
+Version: 2.4
Author: Melograno Ventures
Author URI: https://melograno.io/
Text Domain: ameliabooking
@@ -25,9 +25,11 @@
use AmeliaBookingInfrastructureWPErrorServiceErrorService;
use AmeliaBookingInfrastructureWPGutenbergBlockAmeliaBookingGutenbergBlock;
use AmeliaBookingInfrastructureWPGutenbergBlockAmeliaStepBookingGutenbergBlock;
+use AmeliaBookingInfrastructureWPGutenbergBlockAmeliaStepBookingButtonGutenbergBlock;
use AmeliaBookingInfrastructureWPGutenbergBlockAmeliaCatalogBookingGutenbergBlock;
use AmeliaBookingInfrastructureWPGutenbergBlockAmeliaCatalogGutenbergBlock;
use AmeliaBookingInfrastructureWPGutenbergBlockAmeliaEventsGutenbergBlock;
+use AmeliaBookingInfrastructureWPGutenbergBlockAmeliaEventsListBookingButtonGutenbergBlock;
use AmeliaBookingInfrastructureWPGutenbergBlockAmeliaEventsListBookingGutenbergBlock;
use AmeliaBookingInfrastructureWPIntegrationsWooCommerceStarterWooCommerceService;
use AmeliaBookingInfrastructureWPSettingsServiceSettingsStorage;
@@ -40,6 +42,7 @@
use Exception;
use SlimApp;
use AmeliaBookingInfrastructureLicence;
+use WPMCPCoreMcpAdapter;
// No direct access
defined('ABSPATH') or die('No script kiddies please!');
@@ -111,7 +114,7 @@
// Const for Amelia version
if (!defined('AMELIA_VERSION')) {
- define('AMELIA_VERSION', '2.3');
+ define('AMELIA_VERSION', '2.4');
}
// Const for site URL
@@ -145,10 +148,6 @@
define('AMELIA_DEV', false);
}
-if (!defined('AMELIA_PRODUCTION')) {
- define('AMELIA_PRODUCTION', true);
-}
-
if (!defined('AMELIA_NGROK_URL')) {
define('AMELIA_NGROK_URL', 'nonmelodiously-barnlike-anika.ngrok-free.dev');
}
@@ -163,7 +162,6 @@
require_once AMELIA_PATH . '/vendor/autoload.php';
-
/**
* @noinspection AutoloadingIssuesInspection
*
@@ -264,10 +262,12 @@
// Register Gutenberg blocks for rendering on frontend (works for all users, logged in or not)
AmeliaStepBookingGutenbergBlock::init();
+ AmeliaStepBookingButtonGutenbergBlock::init();
AmeliaCatalogBookingGutenbergBlock::init();
AmeliaBookingGutenbergBlock::init();
AmeliaCatalogGutenbergBlock::init();
AmeliaEventsGutenbergBlock::init();
+ AmeliaEventsListBookingButtonGutenbergBlock::init();
AmeliaEventsListBookingGutenbergBlock::init();
// Init menu if user is logged in with amelia role
@@ -674,6 +674,86 @@
AMELIA_VERSION
);
}
+
+ /**
+ * Resolve AutoUpdateHook without triggering autoload when files are partially removed (e.g. during uninstall).
+ *
+ * @return class-string|null Fully-qualified class name if loadable, otherwise null.
+ */
+ private static function getAutoUpdateHookClass()
+ {
+ $class = __NAMESPACE__ . 'InfrastructureWPInstallActionsAutoUpdateHook';
+ if (class_exists($class, false)) {
+ return $class;
+ }
+ $path = AMELIA_PATH . '/src/Infrastructure/WP/InstallActions/AutoUpdateHook.php';
+ if (!is_file($path)) {
+ return null;
+ }
+ require_once $path;
+ return class_exists($class, false) ? $class : null;
+ }
+
+ /**
+ * Update transient filter — must not reference AutoUpdateHook directly in add_filter (invalid callback if class file is missing during delete).
+ *
+ * @param mixed $transient
+ *
+ * @return mixed
+ */
+ public static function filterPreSetSiteTransientUpdatePlugins($transient)
+ {
+ $class = self::getAutoUpdateHookClass();
+ if ($class === null) {
+ return $transient;
+ }
+ return $class::checkUpdate($transient);
+ }
+
+ /**
+ *
+ * @param false|object|array $response
+ * @param string $action
+ * @param object $args
+ *
+ * @return mixed
+ */
+ public static function filterPluginsApi($response, $action, $args)
+ {
+ $class = self::getAutoUpdateHookClass();
+ if ($class === null) {
+ return $response;
+ }
+ return $class::checkInfo($response, $action, $args);
+ }
+
+ /**
+ */
+ public static function actionInPluginUpdateMessage()
+ {
+ $class = self::getAutoUpdateHookClass();
+ if ($class === null) {
+ return;
+ }
+ $class::addMessageOnPluginsPage();
+ }
+
+ /**
+ * @param bool|WP_Error $reply
+ * @param string $package
+ * @param WP_Upgrader $updater
+ * @param mixed $extra
+ *
+ * @return bool|WP_Error|string
+ */
+ public static function filterUpgraderPreDownload($reply, $package, $updater, $extra = null)
+ {
+ $class = self::getAutoUpdateHookClass();
+ if ($class === null) {
+ return $reply;
+ }
+ return $class::addMessageOnUpdate($reply, $package, $updater);
+ }
}
add_action('wp_ajax_amelia_remove_wpdt_promo_notice', array('AmeliaBookingPlugin', 'amelia_remove_wpdt_promo_notice'));
@@ -708,7 +788,7 @@
/** Activation hook for new site on multisite setup */
add_action('wpmu_new_blog', array('AmeliaBookingInfrastructureWPInstallActionsActivationNewSiteMultisite', 'init'));
-/** Define the API for updating checking */
+/** Define the API for updating checking (callbacks on Plugin so hooks stay valid if AutoUpdateHook cannot load — e.g. partial delete) */
/** Define the alternative response for information checking */
@@ -732,3 +812,11 @@
if (function_exists('is_plugin_active') && is_plugin_active('angie/angie.php')) {
add_action('admin_enqueue_scripts', array('AmeliaBookingPlugin', 'enqueueAngieMcpServer'));
}
+
+if (class_exists(McpAdapter::class)) {
+ McpAdapter::instance();
+
+ add_action('mcp_adapter_init', array('AmeliaBookingInfrastructureWPMCPAmeliaMcpServerRegistrar', 'init'));
+ add_action('wp_abilities_api_categories_init', array('AmeliaBookingInfrastructureWPMCPAmeliaAbilitiesRegistrar', 'registerCategories'));
+ add_action('wp_abilities_api_init', array('AmeliaBookingInfrastructureWPMCPAmeliaAbilitiesRegistrar', 'registerAbilities'));
+}
--- a/ameliabooking/extensions/divi_5_amelia/server/AmeliaBookingButtonRendererTrait.php
+++ b/ameliabooking/extensions/divi_5_amelia/server/AmeliaBookingButtonRendererTrait.php
@@ -0,0 +1,224 @@
+<?php
+
+namespace Divi5Amelia;
+
+use ETBuilderFrontEndBlockParserBlockParserStore;
+use ETBuilderPackagesModuleModule;
+use ETBuilderPackagesModuleLayoutComponentsModuleElementsModuleElements;
+use WP_Block;
+
+/**
+ * Shared helpers for rendering Divi-trigger buttons used by Amelia modules.
+ */
+trait AmeliaBookingButtonRendererTrait
+{
+ /**
+ * Normalize button innerContent.desktop.value to object-like array.
+ * Supports both persisted string and structured array formats.
+ */
+ private static function normalizeButtonDesktopValue(array &$button_attr): array
+ {
+ $button_attr['innerContent'] = $button_attr['innerContent'] ?? [];
+ $button_attr['innerContent']['desktop'] = $button_attr['innerContent']['desktop'] ?? [];
+
+ $raw = $button_attr['innerContent']['desktop']['value'] ?? null;
+
+ if (is_string($raw)) {
+ $button_attr['innerContent']['desktop']['value'] = [
+ 'text' => $raw,
+ ];
+ } elseif (!is_array($raw)) {
+ $button_attr['innerContent']['desktop']['value'] = [];
+ }
+
+ return $button_attr['innerContent']['desktop']['value'];
+ }
+
+ /**
+ * Merge CSS class names while preserving uniqueness.
+ */
+ private static function mergeClassNames(string ...$class_names): string
+ {
+ $all = implode(' ', $class_names);
+ $parts = preg_split('/s+/', trim($all)) ?: [];
+ $parts = array_values(array_unique(array_filter($parts, static function ($part) {
+ return $part !== '';
+ })));
+
+ return implode(' ', $parts);
+ }
+
+ /**
+ * Ensure the first rendered button/anchor has display:inline-block in inline style.
+ */
+ private static function ensureInlineBlockDisplayInHtml(string $html): string
+ {
+ if ($html === '') {
+ return $html;
+ }
+
+ // Case 1: Existing style="..." on first clickable tag -> normalize display to inline-block.
+ $updated = preg_replace_callback(
+ '/<(a|button)b([^>]*?)sstyles*=s*(["'])(.*?)3([^>]*)>/i',
+ static function (array $matches): string {
+ $style_value = trim((string) $matches[4]);
+ if (preg_match('/(?:^|;)s*displays*:[^;]*/i', $style_value)) {
+ $style_value = preg_replace(
+ '/(^|;)s*displays*:[^;]*(?:;|$)/i',
+ '$1 display: inline-block;',
+ $style_value,
+ 1
+ ) ?? $style_value;
+ } else {
+ if ($style_value !== '' && substr($style_value, -1) !== ';') {
+ $style_value .= ';';
+ }
+ $style_value .= ' display: inline-block;';
+ }
+
+ if ($style_value !== '' && substr($style_value, -1) !== ';') {
+ $style_value .= ';';
+ }
+
+ return '<' . $matches[1] . $matches[2] . ' style=' . $matches[3] . trim($style_value) . $matches[3] . $matches[5] . '>';
+ },
+ $html,
+ 1
+ );
+
+ if ($updated !== null && $updated !== $html) {
+ return $updated;
+ }
+
+ // Case 2: Bare style attribute token (style without value) on first clickable tag.
+ $updated = preg_replace(
+ '/(<(?:a|button)b[^>]*?)sstyle(?=[s>])/i',
+ '$1 style="display: inline-block;"',
+ $html,
+ 1
+ );
+
+ if ($updated !== null && $updated !== $html) {
+ return $updated;
+ }
+
+ // Case 3: No style attribute on first clickable tag -> inject one.
+ $updated = preg_replace(
+ '/(<(?:a|button)b)([s>])/i',
+ '$1 style="display: inline-block;"$2',
+ $html,
+ 1
+ );
+
+ return $updated ?? $html;
+ }
+
+ /**
+ * Build and render trigger button HTML while keeping Divi button markup/styles.
+ */
+ private static function renderTriggerButtonHtml(
+ array $attrs,
+ WP_Block $block,
+ ModuleElements $elements,
+ string $auto_trigger,
+ string $button_base_class,
+ string $fallback_button_text
+ ): string {
+ $button_attr = $attrs['button'] ?? [];
+ $desktop_value = self::normalizeButtonDesktopValue($button_attr);
+ $button_text = trim((string) ($desktop_value['text'] ?? ''));
+ if ($button_text === '') {
+ $button_text = $fallback_button_text;
+ }
+
+ $module_id = isset($block->parsed_block['id']) ? (string) $block->parsed_block['id'] : '';
+ $divi_button_instance_class = 'et_pb_button_' . sanitize_html_class($module_id !== '' ? $module_id : wp_unique_id());
+
+ $button_attr['attributes'] = $button_attr['attributes'] ?? [];
+ $existing_button_class = isset($button_attr['attributes']['class']) ? trim((string) $button_attr['attributes']['class']) : '';
+ $button_attr['attributes']['class'] = self::mergeClassNames(
+ $button_base_class,
+ 'et_block_module',
+ $divi_button_instance_class,
+ $existing_button_class
+ );
+
+ $button_attr['attributes']['id'] = trim($auto_trigger);
+ if ($module_id !== '' && !isset($button_attr['attributes']['data-id'])) {
+ $button_attr['attributes']['data-id'] = $module_id;
+ }
+
+ $button_attr['innerContent']['desktop']['value']['text'] = $button_text;
+ if (!isset($button_attr['innerContent']['desktop']['value']['linkUrl'])) {
+ $button_attr['innerContent']['desktop']['value']['linkUrl'] = '';
+ }
+
+ $button_html = $elements->render([
+ 'attrName' => 'button',
+ 'elementAttr' => $button_attr,
+ 'elementProps' => [
+ 'hasWrapper' => false,
+ 'allowEmptyUrl' => true,
+ ],
+ ]);
+
+ $button_html = self::ensureInlineBlockDisplayInHtml((string) $button_html);
+
+ if (!empty($button_html) && !empty($auto_trigger)) {
+ $button_html = preg_replace(
+ '/(<(?:a|button)b)([s>])/i',
+ '$1 id="' . esc_attr($auto_trigger) . '"$2',
+ $button_html,
+ 1
+ );
+ }
+
+ return (string) $button_html;
+ }
+
+ /**
+ * Render the common module shell around the booking trigger button.
+ */
+ private static function renderBookingButtonModuleShell(
+ array $attrs,
+ WP_Block $block,
+ ModuleElements $elements,
+ string $button_html,
+ string $wrapper_class,
+ string $shortcode_hidden_content
+ ): string {
+ $parent = BlockParserStore::get_parent(
+ $block->parsed_block['id'],
+ $block->parsed_block['storeInstance']
+ );
+
+ $inner_content =
+ $elements->style_components(['attrName' => 'module'])
+ . $elements->style_components(['attrName' => 'button'])
+ . '<div class="' . esc_attr($wrapper_class) . '">'
+ . $button_html
+ . '</div>'
+ . '<div class="amelia-shortcode" style="display:none">' . $shortcode_hidden_content . '</div>';
+
+ return Module::render([
+ // FE only.
+ 'orderIndex' => $block->parsed_block['orderIndex'],
+ 'storeInstance' => $block->parsed_block['storeInstance'],
+
+ // VB equivalent.
+ 'attrs' => $attrs,
+ 'elements' => $elements,
+ 'id' => $block->parsed_block['id'],
+ 'name' => $block->block_type->name,
+ 'classnamesFunction' => [self::class, 'module_classnames'],
+ 'moduleCategory' => $block->block_type->category,
+ 'stylesComponent' => [self::class, 'module_styles'],
+ 'scriptDataComponent' => [self::class, 'module_script_data'],
+ 'parentAttrs' => $parent->attrs ?? [],
+ 'parentId' => $parent->id ?? '',
+ 'parentName' => $parent->blockName ?? '',
+ 'children' => $inner_content,
+ ]);
+ }
+}
+
--- a/ameliabooking/extensions/divi_5_amelia/server/AmeliaEventsListBookingButtonModule.php
+++ b/ameliabooking/extensions/divi_5_amelia/server/AmeliaEventsListBookingButtonModule.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace Divi5Amelia;
+
+use ETBuilderFrameworkDependencyManagementInterfacesDependencyInterface;
+use ETBuilderFrontEndModuleStyle;
+use ETBuilderPackagesModuleLayoutComponentsModuleElementsModuleElements;
+use ETBuilderPackagesModuleOptionsElementElementClassnames;
+use ETBuilderPackagesModuleLibraryModuleRegistration;
+use AmeliaBookingInfrastructureWPTranslationsLiteBackendStrings;
+use WP_Block;
+
+/**
+ * Class that handles "Amelia Events List Booking Button" module output in frontend.
+ */
+class AmeliaEventsListBookingButtonModule implements DependencyInterface
+{
+ use AmeliaBookingButtonRendererTrait;
+
+ private const BUTTON_BASE_CLASS = 'amelia-eventslist-booking-button';
+
+ /**
+ * Register module.
+ */
+ public function load()
+ {
+ add_action('init', [AmeliaEventsListBookingButtonModule::class, 'registerModule']);
+ }
+
+ public static function registerModule()
+ {
+ $module_json_folder_path = dirname(__DIR__, 1) . '/visual-builder/src/modules/EventsListBookingButton';
+
+ ModuleRegistration::register_module(
+ $module_json_folder_path,
+ [
+ 'render_callback' => [AmeliaEventsListBookingButtonModule::class, 'renderCallback'],
+ ]
+ );
+ }
+
+ /**
+ * Generate classnames for the module.
+ */
+ public static function module_classnames(array $args): void
+ {
+ $classnames_instance = $args['classnamesInstance'];
+ $attrs = $args['attrs'];
+
+ $classnames_instance->add(
+ ElementClassnames::classnames([
+ 'attrs' => $attrs['module']['decoration'] ?? [],
+ ])
+ );
+ }
+
+ /**
+ * Module script data.
+ */
+ public static function module_script_data(array $args): void
+ {
+ $elements = $args['elements'];
+
+ $elements->script_data([
+ 'attrName' => 'module',
+ ]);
+ }
+
+ /**
+ * Module styles.
+ */
+ public static function module_styles(array $args): void
+ {
+ $attrs = $args['attrs'] ?? [];
+ $elements = $args['elements'];
+ $settings = $args['settings'] ?? [];
+
+ Style::add([
+ 'id' => $args['id'],
+ 'name' => $args['name'],
+ 'orderIndex' => $args['orderIndex'],
+ 'storeInstance' => $args['storeInstance'],
+ 'styles' => [
+ // Module styles.
+ $elements->style([
+ 'attrName' => 'module',
+ 'styleProps' => [
+ 'disabledOn' => [
+ 'disabledModuleVisibility' => $settings['disabledModuleVisibility'] ?? null,
+ ],
+ ],
+ ]),
+ // Button styles.
+ $elements->style([
+ 'attrName' => 'button',
+ ]),
+ ],
+ ]);
+ }
+
+ /**
+ * Render module HTML output.
+ */
+ public static function renderCallback(array $attrs, string $content, WP_Block $block, ModuleElements $elements): string
+ {
+ $auto_trigger = wp_unique_id('amelia-eventslist-booking-btn-');
+
+ $shortcode = '[ameliaeventslistbooking';
+ $shortcode .= ' trigger=' . sanitize_html_class($auto_trigger);
+ $shortcode .= ' trigger_type=id';
+ $shortcode .= ' in_dialog=1';
+
+ // Preselect/filter parameters
+ $booking_params = $attrs['booking_params']['innerContent']['desktop']['value'] ?? false;
+ if ($booking_params === 'on') {
+ $event = $attrs['events']['innerContent']['desktop']['value'] ?? [];
+ if ($event && count($event) > 0) {
+ $shortcode .= ' event=' . implode(',', array_filter(array_map('absint', $event)));
+ }
+
+ $event_to_show = $attrs['event_to_show']['innerContent']['desktop']['value'] ?? 'all';
+ if (count($event) === 0 && $event_to_show !== 'all') {
+ if ($event_to_show === 'custom') {
+ $start_date = $attrs['start_date']['innerContent']['desktop']['value'] ?? '';
+ $end_date = $attrs['end_date']['innerContent']['desktop']['value'] ?? '';
+
+ $has_valid_start = $start_date && preg_match('/^d{4}-d{2}-d{2}$/', $start_date);
+ $has_valid_end = $end_date && preg_match('/^d{4}-d{2}-d{2}$/', $end_date);
+
+ if (!$has_valid_start || !$has_valid_end) {
+ // Fallback: render without date range filter, or log warning
+ // For now, skip the range parameter rather than hiding the button
+ $event_to_show = 'all';
+ } else {
+ $shortcode .= ' range="' . esc_attr($start_date) . ' - ' . esc_attr($end_date) . '"';
+ }
+ } else {
+ $shortcode .= ' range="' . esc_attr($event_to_show) . '"';
+ }
+ }
+
+ $tag = $attrs['tags']['innerContent']['desktop']['value'] ?? [];
+ if ($tag && count($tag) > 0) {
+ $shortcode .= ' tag="' . implode(',', array_map(function ($t) {
+ return '{' . sanitize_text_field($t) . '}';
+ }, $tag)) . '"';
+ }
+
+ $recurring = $attrs['recurring']['innerContent']['desktop']['value'] ?? false;
+ if ($recurring === 'on') {
+ $shortcode .= ' recurring=1';
+ }
+
+ $locations = $attrs['locations']['innerContent']['desktop']['value'] ?? [];
+ if ($locations && count($locations) > 0) {
+ $shortcode .= ' location=' . implode(',', array_filter(array_map('absint', $locations)));
+ }
+ }
+
+ $shortcode .= ']';
+
+ // Render shortcode now to preserve quoted attributes (e.g., custom range) reliably.
+ $shortcode_output = do_shortcode($shortcode);
+
+ $button_html = self::renderTriggerButtonHtml(
+ $attrs,
+ $block,
+ $elements,
+ $auto_trigger,
+ self::BUTTON_BASE_CLASS,
+ LiteBackendStrings::get('event_book_event')
+ );
+
+ return self::renderBookingButtonModuleShell(
+ $attrs,
+ $block,
+ $elements,
+ $button_html,
+ 'amelia-eventslist-booking-button_wrapper',
+ $shortcode_output
+ );
+ }
+}
+
--- a/ameliabooking/extensions/divi_5_amelia/server/AmeliaStepBookingButtonModule.php
+++ b/ameliabooking/extensions/divi_5_amelia/server/AmeliaStepBookingButtonModule.php
@@ -0,0 +1,209 @@
+<?php
+
+namespace Divi5Amelia;
+
+use ETBuilderFrameworkDependencyManagementInterfacesDependencyInterface;
+use ETBuilderFrontEndModuleStyle;
+use ETBuilderPackagesModuleLayoutComponentsModuleElementsModuleElements;
+use ETBuilderPackagesModuleOptionsElementElementClassnames;
+use ETBuilderPackagesModuleLibraryModuleRegistration;
+use WP_Block;
+
+/**
+ * Class that handles "Amelia Step Booking Button" module output in frontend.
+ */
+class AmeliaStepBookingButtonModule implements DependencyInterface
+{
+ use AmeliaBookingButtonRendererTrait;
+
+ private const BUTTON_BASE_CLASS = 'amelia-step-booking-button';
+
+ /**
+ * Register module.
+ */
+ public function load()
+ {
+ add_action('init', [AmeliaStepBookingButtonModule::class, 'registerModule']);
+ }
+
+ public static function registerModule()
+ {
+ $module_json_folder_path = dirname(__DIR__, 1) . '/visual-builder/src/modules/StepBookingButton';
+
+ ModuleRegistration::register_module(
+ $module_json_folder_path,
+ [
+ 'render_callback' => [AmeliaStepBookingButtonModule::class, 'renderCallback'],
+ ]
+ );
+ }
+
+ /**
+ * Generate classnames for the module.
+ */
+ public static function module_classnames(array $args): void
+ {
+ $classnames_instance = $args['classnamesInstance'];
+ $attrs = $args['attrs'];
+
+ $classnames_instance->add(
+ ElementClassnames::classnames([
+ 'attrs' => $attrs['module']['decoration'] ?? [],
+ ])
+ );
+ }
+
+ /**
+ * Module script data.
+ */
+ public static function module_script_data(array $args): void
+ {
+ $elements = $args['elements'];
+
+ $elements->script_data([
+ 'attrName' => 'module',
+ ]);
+ }
+
+ /**
+ * Module styles.
+ */
+ public static function module_styles(array $args): void
+ {
+ $attrs = $args['attrs'] ?? [];
+ $elements = $args['elements'];
+ $settings = $args['settings'] ?? [];
+
+ Style::add([
+ 'id' => $args['id'],
+ 'name' => $args['name'],
+ 'orderIndex' => $args['orderIndex'],
+ 'storeInstance' => $args['storeInstance'],
+ 'styles' => [
+ // Module styles.
+ $elements->style([
+ 'attrName' => 'module',
+ 'styleProps' => [
+ 'disabledOn' => [
+ 'disabledModuleVisibility' => $settings['disabledModuleVisibility'] ?? null,
+ ],
+ ],
+ ]),
+ // Button styles.
+ $elements->style([
+ 'attrName' => 'button',
+ ]),
+ ],
+ ]);
+ }
+
+ /**
+ * Normalize toggle values coming from Divi attrs.
+ */
+ private static function isToggleEnabled($value): bool
+ {
+ if (true === $value || 1 === $value || '1' === $value || 'on' === $value || 'true' === $value) {
+ return true;
+ }
+
+ if (is_array($value)) {
+ if (isset($value['desktop']['value'])) {
+ return self::isToggleEnabled($value['desktop']['value']);
+ }
+
+ if (isset($value['value'])) {
+ return self::isToggleEnabled($value['value']);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Render module HTML output.
+ */
+ public static function renderCallback(array $attrs, string $content, WP_Block $block, ModuleElements $elements): string
+ {
+ $auto_trigger = wp_unique_id('amelia-step-booking-btn-');
+
+ $shortcode = '[ameliastepbooking';
+ $shortcode .= ' trigger=' . sanitize_html_class($auto_trigger);
+ $shortcode .= ' trigger_type=id';
+ $shortcode .= ' in_dialog=1';
+
+ $layout = $attrs['layout']['innerContent']['desktop']['value'] ?? '1';
+ if ($layout !== null && $layout !== '') {
+ $shortcode .= ' layout=' . absint($layout);
+ }
+
+ $to_csv_ids = static function ($value): string {
+ $vals = is_array($value) ? $value : [$value];
+ $vals = array_filter(array_map('absint', $vals));
+
+ return implode(',', $vals);
+ };
+
+ $preselect = $attrs['parameters']['innerContent']['desktop']['value']
+ ?? $attrs['parameters']['innerContent']['value']
+ ?? $attrs['parameters']['innerContent']
+ ?? false;
+
+ if (self::isToggleEnabled($preselect)) {
+ $show = $attrs['type']['innerContent']['desktop']['value'] ?? '';
+ if ($show !== '' && $show !== '0') {
+ $shortcode .= ' show=' . sanitize_key((string) $show);
+ }
+
+ $category = $attrs['categories']['innerContent']['desktop']['value'] ?? [];
+ $service = $attrs['services']['innerContent']['desktop']['value'] ?? [];
+ $employee = $attrs['employees']['innerContent']['desktop']['value'] ?? [];
+ $location = $attrs['locations']['innerContent']['desktop']['value'] ?? [];
+ $package = $attrs['packages']['innerContent']['desktop']['value'] ?? [];
+
+ $service_csv = $to_csv_ids($service);
+ $category_csv = $to_csv_ids($category);
+ $employee_csv = $to_csv_ids($employee);
+ $location_csv = $to_csv_ids($location);
+ $package_csv = $to_csv_ids($package);
+
+ if ($service_csv !== '') {
+ $shortcode .= ' service=' . $service_csv;
+ } elseif ($category_csv !== '') {
+ $shortcode .= ' category=' . $category_csv;
+ }
+
+ if ($employee_csv !== '') {
+ $shortcode .= ' employee=' . $employee_csv;
+ }
+
+ if ($location_csv !== '') {
+ $shortcode .= ' location=' . $location_csv;
+ }
+
+ if ($package_csv !== '') {
+ $shortcode .= ' package=' . $package_csv;
+ }
+ }
+
+ $shortcode .= ']';
+
+ $button_html = self::renderTriggerButtonHtml(
+ $attrs,
+ $block,
+ $elements,
+ $auto_trigger,
+ self::BUTTON_BASE_CLASS,
+ __('Book Appointment', 'wpamelia')
+ );
+
+
+ return self::renderBookingButtonModuleShell(
+ $attrs,
+ $block,
+ $elements,
+ $button_html,
+ 'amelia-step-booking-button_wrapper',
+ esc_html($shortcode)
+ );
+ }
+}
--- a/ameliabooking/extensions/divi_5_amelia/server/index.php
+++ b/ameliabooking/extensions/divi_5_amelia/server/index.php
@@ -123,11 +123,16 @@
require_once $divi_dependency_interface;
+ // Shared helpers used by booking button modules.
+ require_once __DIR__ . '/AmeliaBookingButtonRendererTrait.php';
+
require_once __DIR__ . '/AmeliaStepBookingModule.php';
+ require_once __DIR__ . '/AmeliaStepBookingButtonModule.php';
require_once __DIR__ . '/AmeliaBookingModule.php';
require_once __DIR__ . '/AmeliaCatalogBookingModule.php';
require_once __DIR__ . '/AmeliaCatalogModule.php';
require_once __DIR__ . '/AmeliaEventsListModule.php';
+ require_once __DIR__ . '/AmeliaEventsListBookingButtonModule.php';
require_once __DIR__ . '/AmeliaEventsModule.php';
require_once __DIR__ . '/AmeliaSearchModule.php';
@@ -147,6 +152,13 @@
add_action(
'divi_module_library_modules_dependency_tree',
function ($dependency_tree) {
+ $dependency_tree->add_dependency(new AmeliaStepBookingButtonModule());
+ }
+ );
+
+ add_action(
+ 'divi_module_library_modules_dependency_tree',
+ function ($dependency_tree) {
$dependency_tree->add_dependency(new AmeliaBookingModule());
}
);
@@ -172,6 +184,13 @@
}
);
+ add_action(
+ 'divi_module_library_modules_dependency_tree',
+ function ($dependency_tree) {
+ $dependency_tree->add_dependency(new AmeliaEventsListBookingButtonModule());
+ }
+ );
+
add_action(
'divi_module_library_modules_dependency_tree',
function ($dependency_tree) {
--- a/ameliabooking/src/Application/Commands/Booking/Appointment/GetTimeSlotsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Appointment/GetTimeSlotsCommandHandler.php
@@ -11,6 +11,7 @@
use AmeliaBookingDomainEntityBookableServiceService;
use AmeliaBookingDomainEntityBookingSlotsEntities;
use AmeliaBookingDomainServicesDateTimeDateTimeService;
+use AmeliaBookingDomainServicesScheduleScheduleService;
use AmeliaBookingDomainServicesSettingsSettingsService;
use AmeliaBookingDomainValueObjectsPositiveDuration;
use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
@@ -98,6 +99,29 @@
$settings = $applicationTimeSlotService->getSlotsSettings($isFrontEndBooking, $slotsEntities, $props);
+ if (!empty($settings['allowAdminBookAtAnyTime']) && !empty($props['structured'])) {
+ /** @var ScheduleService $scheduleService */
+ $scheduleService = $this->container->get('domain.schedule.service');
+ $normalProvidersIntervals = [];
+ foreach ($slotsEntities->getProviders()->getItems() as $provider) {
+ $normalProvidersIntervals[$provider->getId()->getValue()] = [
+ 'weekDays' => $scheduleService->getProviderWeekDaysIntervals(
+ $provider,
+ $slotsEntities->getLocations(),
+ $props['locationId'],
+ $props['serviceId']
+ ),
+ 'specialDays' => $scheduleService->getProviderSpecialDayIntervals(
+ $provider,
+ $slotsEntities->getLocations(),
+ $props['locationId'],
+ $props['serviceId']
+ ),
+ ];
+ }
+ $settings['normalProvidersIntervals'] = $normalProvidersIntervals;
+ }
+
$lastBookedProviderId = null;
/** @var SlotsEntities $filteredSlotEntities */
--- a/ameliabooking/src/Application/Commands/Calendar/GetCalendarEventsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Calendar/GetCalendarEventsCommandHandler.php
@@ -11,8 +11,8 @@
use AmeliaBookingApplicationCommandsCommandResult;
use AmeliaBookingApplicationCommonExceptionsAccessDeniedException;
use AmeliaBookingApplicationServicesBookingEventApplicationService;
+use AmeliaBookingApplicationServicesCalendarCalendarProviderService;
use AmeliaBookingApplicationServicesUserProviderApplicationService;
-use AmeliaBookingDomainCollectionCollection;
use AmeliaBookingDomainCommonExceptionsInvalidArgumentException;
use AmeliaBookingDomainEntityBookingAppointmentAppointment;
use AmeliaBookingDomainEntityBookingEventEvent;
@@ -72,6 +72,12 @@
$timeZone = $providerAS->getTimeZone($user);
}
+ /** @var CalendarProviderService $calendarProviderService */
+ $calendarProviderService = $this->container->get('application.calendar.provider.service');
+ $resourceTimeGridProviderIds = (($queryParams['view'] ?? '') === 'resourceTimeGridDay')
+ ? $calendarProviderService->getVisibleProviderIds($queryParams)
+ : [];
+
$sortedItems = array_merge(
$this->getAppointments($queryParams, $timeZone),
$this->getEvents($queryParams, $timeZone),
@@ -112,7 +118,11 @@
if ($isAppointment) {
$filledDays[$itemStartDate]['events'][] = $this->appointmentFormatter($item, $user);
} elseif ($isBlockTime) {
- $filledDays[$itemStartDate]['events'][] = $this->blockTimeFormatter($item);
+ $filledDays[$itemStartDate]['events'][] = $this->blockTimeFormatter(
+ $item,
+ $queryParams,
+ $resourceTimeGridProviderIds
+ );
} else {
$filledDays[$itemStartDate]['events'][] = $this->eventFormatter($item['event'], $item['eventPeriod'], $queryParams);
}
@@ -304,6 +314,7 @@
$appointment->getInternalNotes()->getValue()
: '',
'integrationCalendarType' => false,
+ 'resourceId' => $appointment->getProvider()->getId()->getValue(),
'type' => $appointment->getBookings()->length() === 1
? 'singleAppointment'
: 'groupAppointment',
@@ -339,6 +350,7 @@
'notes' => '',
'locationName' => $eventEntity->getLocation() ? $eventEntity->getLocation()->getName()->getValue() : '',
'employeeName' => $eventEntity->getOrganizer() ? $eventEntity->getOrganizer()->getFullName() : '',
+ 'resourceId' => $eventEntity->getOrganizer() ? $eventEntity->getOrganizer()->getId()->getValue() : null,
];
if (in_array($queryParams['view'], ['dayGridMonthSevenDays', 'dayGridMonth', 'dayGridMonthMobile'])) {
@@ -357,12 +369,15 @@
return $event;
}
- private function blockTimeFormatter(BlockTime $blockTime): array
- {
+ private function blockTimeFormatter(
+ BlockTime $blockTime,
+ array $queryParams,
+ array $resourceTimeGridProviderIds
+ ): array {
$startDate = $blockTime->getStartDate()->getValue();
$endDate = $blockTime->getEndDate()->getValue();
- return [
+ $event = [
'uuid' => $blockTime->getId()->getValue(),
'id' => $blockTime->getId()->getValue(),
'title' => $blockTime->getName()->getValue(),
@@ -374,6 +389,17 @@
'endWithoutBuffer' => $endDate->format('Y-m-d H:i:s'),
'timeZone' => $startDate->getTimezone()->getName(),
'employeeName' => $blockTime->getUser() ? $blockTime->getUser()->getFullName() : BackendStrings::get('all_employees'),
+ 'resourceId' => $blockTime->getUser() ? $blockTime->getUser()->getId()->getValue() : null,
];
+
+ if (
+ ($queryParams['view'] ?? '') === 'resourceTimeGridDay' &&
+ $blockTime->getUser() === null &&
+ $resourceTimeGridProviderIds !== []
+ ) {
+ $event['resourceIds'] = array_map('strval', $resourceTimeGridProviderIds);
+ }
+
+ return $event;
}
}
--- a/ameliabooking/src/Application/Commands/Calendar/GetCalendarSlotsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Calendar/GetCalendarSlotsCommandHandler.php
@@ -9,6 +9,7 @@
use AmeliaBookingApplicationCommandsCommandHandler;
use AmeliaBookingApplicationCommandsCommandResult;
+use AmeliaBookingApplicationServicesCalendarCalendarProviderService;
use AmeliaBookingApplicationServicesUserProviderApplicationService;
use AmeliaBookingDomainEntityEntities;
use AmeliaBookingDomainCollectionCollection;
@@ -22,7 +23,6 @@
use AmeliaBookingDomainServicesDateTimeDateTimeService;
use AmeliaBookingDomainValueObjectsDateTimeDateTimeValue;
use AmeliaBookingDomainValueObjectsStringBookingStatus;
-use AmeliaBookingDomainValueObjectsStringStatus;
use AmeliaVendorPsrContainerContainerExceptionInterface;
use DateInterval;
use DateInvalidTimeZoneException;
@@ -41,8 +41,8 @@
{
$result = new CommandResult();
- $providerRepository = $this->container->get('domain.users.providers.repository');
- $locationRepository = $this->container->get('domain.locations.repository');
+ /** @var CalendarProviderService $calendarProviderService */
+ $calendarProviderService = $this->container->get('application.calendar.provider.service');
$this->userTimezone = DateTimeService::getTimeZone()->getName();
@@ -54,41 +54,48 @@
$this->userTimezone = $providerAS->getTimeZone($user);
}
- $queryParams = $command->getField('queryParams');
$allWorkDays = [];
- $selectedService = $queryParams['service'] ?? null;
-
- $queryParams['locations'] = array_map(
- fn($location) => $location['id'],
- $locationRepository->getFiltered(
- ['status' => !empty($queryParams['providers']) ? null : Status::VISIBLE],
- 0
- )->toArray()
- );
-
- $criteria = ['providerStatus' => !empty($queryParams['providers']) ? null : Status::VISIBLE];
- foreach ($queryParams as $key => $value) {
- if ($key !== 'providerStatus') {
- $criteria[$key] = $value;
- }
- }
+ $resources = [];
+ $formattedWorkPeriods = [];
+ $queryParams = $command->getField('queryParams');
+ $isResourceView = ($queryParams['view'] ?? '') === 'resourceTimeGridDay';
- $providers = $providerRepository->getWithSchedule($criteria)->getItems();
+ $providers = $calendarProviderService->getVisibleProviders($queryParams);
foreach ($providers as $provider) {
- if (!$selectedService) {
- $providerWorkDays = $this->getProviderWorkDays($provider, $queryParams);
- $this->getTimeLimitsByProvider($queryParams, $providerWorkDays, $provider);
+ $providerWorkDays = $this->getProviderWorkDays($provider, $queryParams);
+ $this->getTimeLimitsByProvider($queryParams, $providerWorkDays, $provider);
+
+ if (!$isResourceView) {
$this->mergeProviderWorkDays($allWorkDays, $providerWorkDays);
+
+ continue;
+ }
+
+ if (empty($providerWorkDays) || $user->getType() === Entities::CUSTOMER) {
+ $this->fillEmptyWorkDays($providerWorkDays, $queryParams);
}
- }
- if (empty($allWorkDays) || $user->getType() === Entities::CUSTOMER) {
- $this->fillEmptyWorkDays($allWorkDays, $queryParams);
+ $this->processCompanyDaysOff($providerWorkDays, $queryParams);
+ $providerFormatted = $this->formatWorkDays($providerWorkDays, $provider->getId()->getValue());
+ $formattedWorkPeriods = array_merge($formattedWorkPeriods, $providerFormatted);
+ $resources[] = [
+ 'id' => $provider->getId()->getValue(),
+ 'title' => $provider->getFullName(),
+ 'pictureThumbPath' => $provider->getPicture() ? $provider->getPicture()->getThumbPath() : null,
+ 'firstName' => $provider->getFirstName()->getValue(),
+ 'lastName' => $provider->getLastName()->getValue(),
+ ];
}
- $this->processCompanyDaysOff($allWorkDays, $queryParams);
- $formattedWorkPeriods = $this->formatWorkDays($allWorkDays);
+ if (!$isResourceView) {
+ if (empty($allWorkDays) || $user->getType() === Entities::CUSTOMER) {
+ $this->fillEmptyWorkDays($allWorkDays, $queryParams);
+ }
+
+ $this->processCompanyDaysOff($allWorkDays, $queryParams);
+ $formattedWorkPeriods = $this->formatWorkDays($allWorkDays);
+ }
$this->getTimeLimitsFromAppointmentsAndEvents($queryParams);
@@ -96,6 +103,7 @@
'workPeriods' => $formattedWorkPeriods,
'slotMinTime' => $this->timeLimits['slotMinTime'],
'slotMaxTime' => $this->timeLimits['slotMaxTime'],
+ 'resources' => $resources,
'now' => DateTimeService::getNowDateTime()
]);
@@ -107,11 +115,12 @@
*/
private function fillEmptyWorkDays(array &$allWorkDays, array $queryParams): void
{
- [$this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']] = $this->getLimitsFromCompanyWorkHours();
+ if ($this->timeLimits['slotMinTime'] === '24:00:00' && $this->timeLimits['slotMaxTime'] === '00:00:00') {
+ [$this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']] = $this->getLimitsFromCompanyWorkHours();
+ }
if ($this->timeLimits['slotMinTime'] === '24:00:00' && $this->timeLimits['slotMaxTime'] === '00:00:00') {
- $this->timeLimits['slotMinTime'] = '09:00:00';
- $this->timeLimits['slotMaxTime'] = '17:00:00';
+ [$this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']] = ['09:00:00', '17:00:00'];
}
$calendarStartDate = DateTime::createFromFormat('Y-m-d', $queryParams['calendarStartDate']);
@@ -344,14 +353,14 @@
}
}
- private function formatWorkDays(array $allWorkDays): array
+ private function formatWorkDays(array $allWorkDays, ?int $resourceId = null): array
{
$formattedPeriods = [];
foreach ($allWorkDays as $date => $info) {
$periods = $info['periods'];
if (empty($periods)) {
- $formattedPeriods[] = $this->createPeriod($date, $date, 'notWorkHours', 'not-work-hours');
+ $formattedPeriods[] = $this->createPeriod($date, $date, 'notWorkHours', 'not-work-hours', $resourceId);
continue;
}
@@ -362,21 +371,21 @@
$end = "{$date}T{$period['end']}";
if ($i === 0 && $period['start'] !== '00:00:00') {
- $formattedPeriods[] = $this->createPeriod("{$date}T00:00:00", $start, 'notWorkHours', 'not-work-hours');
+ $formattedPeriods[] = $this->createPeriod("{$date}T00:00:00", $start, 'notWorkHours', 'not-work-hours', $resourceId);
}
if ($period['groupId'] === 'dayOff') {
- $formattedPeriods[] = $this->createPeriod($start, $end, 'dayOff', 'day-off');
+ $formattedPeriods[] = $this->createPeriod($start, $end, 'dayOff', 'day-off', $resourceId);
} else {
- $formattedPeriods[] = $this->createPeriod($start, $end, 'workHours', 'work-hours');
+ $formattedPeriods[] = $this->createPeriod($start, $end, 'workHours', 'work-hours', $resourceId);
}
if (isset($periods[$i + 1]) && $period['end'] !== $periods[$i + 1]['start']) {
- $formattedPeriods[] = $this->createPeriod($end, "{$date}T{$periods[$i + 1]['start']}", 'notWorkHours', 'not-work-hours');
+ $formattedPeriods[] = $this->createPeriod($end, "{$date}T{$periods[$i + 1]['start']}", 'notWorkHours', 'not-work-hours', $resourceId);
}
if ($i === count($periods) - 1 && $period['end'] !== '24:00:00') {
- $formattedPeriods[] = $this->createPeriod($end, "{$date}T24:00:00", 'notWorkHours', 'not-work-hours');
+ $formattedPeriods[] = $this->createPeriod($end, "{$date}T24:00:00", 'notWorkHours', 'not-work-hours', $resourceId);
}
}
}
@@ -384,15 +393,21 @@
return $formattedPeriods;
}
- private function createPeriod(string $start, string $end, string $groupId, string $className): array
+ private function createPeriod(string $start, string $end, string $groupId, string $className, ?int $resourceId = null): array
{
- return [
- 'groupId' => $groupId,
- 'start' => $start,
- 'end' => $end,
- 'display' => 'background',
- 'className' => $className
+ $period = [
+ 'groupId' => $groupId,
+ 'start' => $start,
+ 'end' => $end,
+ 'display' => 'background',
+ 'className' => $className,
];
+
+ if ($resourceId !== null) {
+ $period['resourceId'] = $resourceId;
+ }
+
+ return $period;
}
private function getTimeLimitsByProvider(array $queryParams, array $periods, Provider $provider): void
@@ -571,7 +586,7 @@
private function processCompanyDaysOff(array &$allWorkDays, array $queryParams): void
{
$isDateRangeOverlapping = fn(DateTime $start1, DateTime $end1, DateTime $start2, DateTime $end2): bool =>
- $start1 <= $end2 && $end1 >= $start2;
+ $start1 <= $end2 && $end1 >= $start2;
$settingsDS = $this->container->get('domain.settings.service');
$calendarStartDate = DateTime::createFromFormat('Y-m-d', $queryParams['calendarStartDate']);
--- a/ameliabooking/src/Application/Services/Booking/BookingApplicationService.php
+++ b/ameliabooking/src/Application/Services/Booking/BookingApplicationService.php
@@ -17,6 +17,7 @@
use AmeliaBookingDomainEntityEntities;
use AmeliaBookingDomainEntityLocationLocation;
use AmeliaBookingDomainEntityPaymentPayment;
+use AmeliaBookingDomainEntityUserAbstractUser;
use AmeliaBookingDomainEntityUserCustomer;
use AmeliaBookingDomainEntityUserProvider;
use AmeliaBookingDomainFactoryBookableServicePackageFactory;
@@ -298,6 +299,14 @@
$data['utc'] = null;
}
+ if (isset($data['bookings'][0]['customer']) && array_key_exists('type', $data['bookings'][0]['customer'])) {
+ $data['bookings'][0]['customer']['type'] = AbstractUser::USER_ROLE_CUSTOMER;
+ }
+
+ if (isset($data['bookings'][0]['customer']) && array_key_exists('externalId', $data['bookings'][0]['customer'])) {
+ $data['bookings'][0]['customer']['externalId'] = null;
+ }
+
if (!empty($data['bookings'][0]['customer']['firstName'])) {
$data['bookings'][0]['customer']['firstName'] =
sanitize_text_field($data['bookings'][0]['customer']['firstName']);
--- a/ameliabooking/src/Application/Services/Booking/BookingFallbackService.php
+++ b/ameliabooking/src/Application/Services/Booking/BookingFallbackService.php
@@ -81,7 +81,19 @@
'description' => BackendStrings::get('fallback_booking_rejected_desc'),
'color' => '#6c757d',
'icon' => 'info'
- ]
+ ],
+ 'payment_done' => [
+ 'title' => BackendStrings::get('fallback_payment_done_title'),
+ 'description' => BackendStrings::get('fallback_payment_desc'),
+ 'color' => '#dc3545',
+ 'icon' => 'error'
+ ],
+ 'payment_failed' => [
+ 'title' => BackendStrings::get('fallback_payment_failed_title'),
+ 'description' => BackendStrings::get('fallback_payment_desc'),
+ 'color' => '#dc3545',
+ 'icon' => 'error'
+ ],
];
}
--- a/ameliabooking/src/Application/Services/Booking/EventApplicationService.php
+++ b/ameliabooking/src/Application/Services/Booking/EventApplicationService.php
@@ -1925,8 +1925,11 @@
: null;
$waitingCapacity = 0;
+ $isWaitingList = false;
if ($eventSettings && !empty($eventSettings['waitingList']['enabled'])) {
+ $isWaitingList = true;
+
if ($event->getCustomPricing()->getValue()) {
/** @var EventTicket $ticket */
foreach ($event->getCustomTickets()->getItems() as $ticket) {
@@ -1953,6 +1956,7 @@
'approvedStatus' => $approvedStatus,
'maxCapacity' => $event->getMaxCapacity()->getValue(),
'waitingCapacity' => $waitingCapacity,
+ 'isWaitingList' => $isWaitingList,
]
);
}
--- a/ameliabooking/src/Application/Services/Booking/IcsApplicationService.php
+++ b/ameliabooking/src/Application/Services/Booking/IcsApplicationService.php
@@ -21,10 +21,9 @@
use AmeliaBookingInfrastructureRepositoryBookingAppointmentCustomerBookingRepository;
use AmeliaBookingInfrastructureRepositoryLocationLocationRepository;
use AmeliaBookingInfrastructureRepositoryUserUserRepository;
-use EluceoiCalComponentCalendar;
-use EluceoiCalComponentEvent as iCalEvent;
-use EluceoiCalPropertyEventOrganizer as iCalOrganizer;
use AmeliaBookingInfrastructureCommonContainer;
+use AmeliaVendorSabreVObjectComponentVCalendar;
+use AmeliaVendorSabreVObjectUUIDUtil;
use Exception;
use InteropContainerExceptionContainerException;
use AmeliaBookingDomainCommonExceptionsInvalidArgumentException;
@@ -457,54 +456,52 @@
}
/**
- * @param array $periodsData
- * @param bool $separateCalendars
- * @param bool $isTranslation
+ * @param array $periodsData
+ * @param bool $separateCalendars
+ * @param bool $isTranslation
*
* @return array
* @throws Exception
*/
private function getCalendar($periodsData, $separateCalendars, $isTranslation)
{
- $vCalendars = $separateCalendars ? [] : [new Calendar(AMELIA_URL)];
+ $vCalendars = $separateCalendars ? [] : [$this->createVCalendar()];
foreach ($periodsData as $periodData) {
foreach ($periodData['periods'] as $period) {
- $vEvent = new iCalEvent();
+ if ($separateCalendars) {
+ $vCalendar = $this->createVCalendar();
+ $vCalendars[] = $vCalendar;
+ } else {
+ $vCalendar = $vCalendars[0];
+ }
+
+ $vEvent = $vCalendar->add(
+ 'VEVENT',
+ ['UID' => UUIDUtil::getUUID(), 'SUMMARY' => !$isTranslation ? $periodData['name'] : $periodData['nameTr']]
+ );
- $vEvent
- ->setDtStart(new DateTime($period['start'], new DateTimeZone('UTC')))
- ->setDtEnd(new DateTime($period['end'], new DateTimeZone('UTC')))
- ->setSummary(!$isTranslation ? $periodData['name'] : $periodData['nameTr']);
+ $vEvent->add('DTSTART', new DateTime($period['start'], new DateTimeZone('UTC')));
+ $vEvent->add('DTEND', new DateTime($period['end'], new DateTimeZone('UTC')));
if (!empty($periodData['provider'])) {
- $vOrganizer = new iCalOrganizer(
+ $vEvent->add(
+ 'ORGANIZER',
'MAILTO:' . $periodData['provider']['email'],
- array('CN' => $periodData['provider']['fullName'])
+ ['CN' => $periodData['provider']['fullName']]
);
-
- $vEvent->setOrganizer($vOrganizer);
}
if ($periodData['location']) {
- $vEvent->setLocation($periodData['location']);
+ $vEvent->add('LOCATION', $periodData['location']);
}
if ($periodData['description'] || $periodData['descriptionTr']) {
- $vEvent->setDescription(
+ $vEvent->add(
+ 'DESCRIPTION',
!$isTranslation ? $periodData['description'] : $periodData['descriptionTr']
);
}
-
- if ($separateCalendars) {
- $vCalendar = new Calendar(AMELIA_URL);
-
- $vCalendar->addComponent($vEvent);
-
- $vCalendars[] = $vCalendar;
- } else {
- $vCalendars[0]->addComponent($vEvent);
- }
}
}
@@ -512,12 +509,23 @@
foreach ($vCalendars as $index => $vCalendar) {
$result[] = [
- 'name' => sizeof($vCalendars) === 1 ? 'cal.ics' : 'cal' . ($index + 1) . '.ics',
+ 'name' => count($vCalendars) === 1 ? 'cal.ics' : 'cal' . ($index + 1) . '.ics',
'type' => 'text/calendar; charset=utf-8',
- 'content' => $vCalendar->render()
+ 'content' => $vCalendar->serialize(),
];
}
return $result;
}
+
+ /**
+ * @return VCalendar
+ */
+ private function createVCalendar()
+ {
+ $vCalendar = new VCalendar();
+ $vCalendar->PRODID = '-//AMELIA//Amelia//EN';
+
+ return $vCalendar;
+ }
}
--- a/ameliabooking/src/Application/Services/Calendar/CalendarProviderService.php
+++ b/ameliabooking/src/Application/Services/Calendar/CalendarProviderService.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace AmeliaBookingApplicationServicesCalendar;
+
+use AmeliaBookingDomainCommonExceptionsInvalidArgumentException;
+use AmeliaBookingDomainEntityUserProvider;
+use AmeliaBookingDomainValueObjectsStringStatus;
+use AmeliaBookingInfrastructureCommonContainer;
+use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
+use AmeliaBookingInfrastructureRepositoryUserProviderRepository;
+
+class CalendarProviderService
+{
+ /** @var Container */
+ private $container;
+
+ public function __construct(Container $container)
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * Returns providers visible in calendar context with the same criteria used by calendar slots.
+ *
+ * @param array $queryParams
+ * @param bool $includeDatesAsRange
+ *
+ * @return Provider[]
+ * @throws InvalidArgumentException
+ * @throws QueryExecutionException
+ */
+ public function getVisibleProviders(array $queryParams, bool $includeDatesAsRange = false): array
+ {
+ $locationRepository = $this->container->get('domain.locations.repository');
+ /** @var ProviderRepository $providerRepository */
+ $providerRepository = $this->container->get('domain.users.providers.repository');
+
+ $queryParams['locations'] = array_map(
+ static fn($location) => $location['id'],
+ $locationRepository->getFiltered(
+ ['status' => !empty($queryParams['providers']) ? null : Status::VISIBLE],
+ 0
+ )->toArray()
+ );
+
+ if ($includeDatesAsRange) {
+ $queryParams['dates'] = [$queryParams['calendarStartDate'], $queryParams['calendarEndDate']];
+ }
+
+ $criteria = ['providerStatus' => !empty($queryParams['providers']) ? null : Status::VISIBLE];
+ foreach ($queryParams as $key => $value) {
+ if ($key !== 'providerStatus') {
+ $criteria[$key] = $value;
+ }
+ }
+
+ return $providerRepository->getWithSchedule($criteria)->getItems();
+ }
+
+ /**
+ * @param array $queryParams
+ *
+ * @return int[]
+ * @throws InvalidArgumentException
+ * @throws QueryExecutionException
+ */
+ public function getVisibleProviderIds(array $queryParams): array
+ {
+ $providerIds = [];
+
+ foreach ($this->getVisibleProviders($queryParams, true) as $provider) {
+ $providerIds[] = $provider->getId()->getValue();
+ }
+
+ return $providerIds;
+ }
+}
--- a/ameliabooking/src/Application/Services/Helper/HelperService.php
+++ b/ameliabooking/src/Application/Services/Helper/HelperService.php
@@ -249,6 +249,30 @@
}
/**
+ * @param string $locale
+ * @param array $entityTranslation
+ *
+ * @return array|null
+ */
+ private function getTranslation($locale, $entityTranslation)
+ {
+ $localeParts = explode('_', $locale);
+
+ if (!array_key_exists($locale, $entityTranslation) && count($localeParts) > 1) {
+ foreach ($entityTranslation as $key => $value) {
+ if (strpos($key, $localeParts[0] . '_') === 0) {
+ return $value;
+ }
+ }
+ }
+
+ return
+ $entityTranslation &&
+ array_key_exists($locale, $entityTranslation) ?
+ $entityTranslation[$locale] : null;
+ }
+
+ /**
* @param string $locale
* @param string $entityTranslation
* @param string|null $type
@@ -259,21 +283,13 @@
{
$entityTranslation = !empty($entityTranslation) ? json_decode($entityTranslation, true) : null;
- if ($locale) {
- if ($type === null) {
- return
- $entityTranslation &&
- !empty($entityTranslation[$locale]) ?
- $entityTranslation[$locale] : null;
- } else {
- return
- $entityTranslation &&
- !empty($entityTranslation[$type][$locale]) ?
- $entityTranslation[$type][$locale] : null;
- }
- }
+ $translationScope = $type === null
+ ? $entityTranslation
+ : ($entityTranslation[$type] ?? null);
- return null;
+ return $locale && is_array($translationScope)
+ ? $this->getTranslation($locale, $translationScope)
+ : null;
}
/**
--- a/ameliabooking/src/Application/Services/Notification/EmailNotificationService.php
+++ b/ameliabooking/src/Application/Services/Notification/EmailNotificationService.php
@@ -164,7 +164,9 @@
$sendIcs = $settingsService->getSetting('ics', 'sendIcsAtt