Hotel Raxa - Advanced Booking System Implementation

🏨 Hotel Booking Enhancements:
- Implemented Eagle Booking Advanced Pricing add-on
- Added Booking.com-style rate management system
- Created professional calendar interface for pricing
- Integrated deals and discounts functionality

💰 Advanced Pricing Features:
- Dynamic pricing models (per room, per person, per adult)
- Base rates, adult rates, and child rates management
- Length of stay discounts and early bird deals
- Mobile rates and secret deals implementation
- Seasonal promotions and flash sales

📅 Availability Management:
- Real-time availability tracking
- Stop sell and restriction controls
- Closed to arrival/departure functionality
- Minimum/maximum stay requirements
- Automatic sold-out management

💳 Payment Integration:
- Maintained Redsys payment gateway integration
- Seamless integration with existing Eagle Booking
- No modifications to core Eagle Booking plugin

🛠️ Technical Implementation:
- Custom database tables for advanced pricing
- WordPress hooks and filters integration
- AJAX-powered admin interface
- Data migration from existing Eagle Booking
- Professional calendar view for revenue management

📊 Admin Interface:
- Booking.com-style management dashboard
- Visual rate and availability calendar
- Bulk operations for date ranges
- Statistics and analytics dashboard
- Modal dialogs for quick editing

🔧 Code Quality:
- WordPress coding standards compliance
- Secure database operations with prepared statements
- Proper input validation and sanitization
- Error handling and logging
- Responsive admin interface

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hotel Raxa Dev
2025-07-11 07:43:22 +02:00
commit 5b1e2453c7
9816 changed files with 2784509 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Base;
use JsonSerializable;
abstract class Atomic_Control_Base implements JsonSerializable {
private string $bind;
private $label = null;
private $description = null;
abstract public function get_type(): string;
abstract public function get_props(): array;
public static function bind_to( string $prop_name ) {
return new static( $prop_name );
}
protected function __construct( string $prop_name ) {
$this->bind = $prop_name;
}
public function get_bind() {
return $this->bind;
}
public function set_label( string $label ): self {
$this->label = $label;
return $this;
}
public function set_description( string $description ): self {
$this->description = $description;
return $this;
}
public function jsonSerialize(): array {
return [
'type' => 'control',
'value' => [
'type' => $this->get_type(),
'bind' => $this->get_bind(),
'label' => $this->label,
'description' => $this->description,
'props' => $this->get_props(),
],
];
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Base;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Widget_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Atomic_Widget_Base extends Widget_Base {
protected $version = '0.0';
protected $styles = [];
public function __construct( $data = [], $args = null ) {
parent::__construct( $data, $args );
$this->version = $data['version'] ?? '0.0';
$this->styles = $data['styles'] ?? [];
}
public function get_atomic_controls() {
$controls = $this->define_atomic_controls();
return $this->get_valid_controls( $controls );
}
private function get_valid_controls( array $controls ): array {
$valid_controls = [];
$schema = static::get_props_schema();
foreach ( $controls as $control ) {
if ( $control instanceof Section ) {
$cloned_section = clone $control;
$cloned_section->set_items(
$this->get_valid_controls( $control->get_items() )
);
$valid_controls[] = $cloned_section;
continue;
}
if ( ! ( $control instanceof Atomic_Control_Base ) ) {
$this->safe_throw( 'Control must be an instance of `Atomic_Control_Base`.' );
continue;
}
$prop_name = $control->get_bind();
if ( ! $prop_name ) {
$this->safe_throw( 'Control is missing a bound prop from the schema.' );
continue;
}
if ( ! array_key_exists( $prop_name, $schema ) ) {
$this->safe_throw( "Prop `{$prop_name}` is not defined in the schema of `{$this->get_name()}`. Did you forget to define it?" );
continue;
}
$valid_controls[] = $control;
}
return $valid_controls;
}
private function safe_throw( string $message ) {
if ( ! defined( 'ELEMENTOR_DEBUG' ) || ! ELEMENTOR_DEBUG ) {
return;
}
throw new \Exception( $message );
}
abstract protected function define_atomic_controls(): array;
final public function get_controls( $control_id = null ) {
if ( ! empty( $control_id ) ) {
return null;
}
return [];
}
final public function get_initial_config() {
$config = parent::get_initial_config();
$config['atomic_controls'] = $this->get_atomic_controls();
$config['version'] = $this->version;
return $config;
}
final public function get_data_for_save() {
$data = parent::get_data_for_save();
$data['version'] = $this->version;
return $data;
}
final public function get_raw_data( $with_html_content = false ) {
$raw_data = parent::get_raw_data( $with_html_content );
$raw_data['styles'] = $this->styles;
return $raw_data;
}
final public function get_stack( $with_common_controls = true ) {
return [
'controls' => [],
'tabs' => [],
];
}
final public function get_atomic_settings(): array {
$schema = static::get_props_schema();
$raw_settings = $this->get_settings();
$transformed_settings = [];
foreach ( $schema as $key => $prop ) {
if ( array_key_exists( $key, $raw_settings ) ) {
$transformed_settings[ $key ] = $raw_settings[ $key ];
} else {
$transformed_settings[ $key ] = $prop->get_default();
}
$transformed_settings[ $key ] = $this->transform_setting( $transformed_settings[ $key ] );
}
return $transformed_settings;
}
public static function get_props_schema() {
return static::define_props_schema();
}
private function transform_setting( $setting ) {
if ( ! $this->is_transformable( $setting ) ) {
return $setting;
}
switch ( $setting['$$type'] ) {
case 'classes':
return is_array( $setting['value'] )
? join( ' ', $setting['value'] )
: '';
default:
return null;
}
}
private function is_transformable( $setting ): bool {
return ! empty( $setting['$$type'] ) && 'string' === getType( $setting['$$type'] ) && isset( $setting['value'] );
}
abstract protected static function define_props_schema(): array;
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls;
use JsonSerializable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Section implements JsonSerializable {
private $label = null;
private $description = null;
private array $items = [];
public static function make(): self {
return new static();
}
public function set_label( string $label ): self {
$this->label = $label;
return $this;
}
public function set_description( string $description ): self {
$this->description = $description;
return $this;
}
public function set_items( array $items ): self {
$this->items = $items;
return $this;
}
public function get_items() {
return $this->items;
}
public function jsonSerialize(): array {
return [
'type' => 'section',
'value' => [
'label' => $this->label,
'description' => $this->description,
'items' => $this->items,
],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Select_Control extends Atomic_Control_Base {
private array $options = [];
public function get_type(): string {
return 'select';
}
public function set_options( array $options ): self {
$this->options = $options;
return $this;
}
public function get_props(): array {
return [
'options' => $this->options,
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Controls\Types;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Control_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Textarea_Control extends Atomic_Control_Base {
private $placeholder = null;
public function get_type(): string {
return 'textarea';
}
public function set_placeholder( string $placeholder ): self {
$this->placeholder = $placeholder;
return $this;
}
public function get_props(): array {
return [
'placeholder' => $this->placeholder,
];
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Elementor\Modules\AtomicWidgets;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\AtomicWidgets\Widgets\Atomic_Heading;
use Elementor\Modules\AtomicWidgets\Widgets\Atomic_Image;
use Elementor\Plugin;
use Elementor\Widgets_Manager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const EXPERIMENT_NAME = 'atomic_widgets';
const PACKAGES = [
'editor-documents', // TODO: NEED to be removed once the editor will not be dependent on the documents package.
'editor-panels',
'editor-editing-panel',
'editor-style',
];
public function get_name() {
return 'atomic-widgets';
}
public function __construct() {
parent::__construct();
$this->register_experiment();
if ( Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME ) ) {
add_filter( 'elementor/editor/v2/packages', fn( $packages ) => $this->add_packages( $packages ) );
add_filter( 'elementor/widgets/register', fn( Widgets_Manager $widgets_manager ) => $this->register_widgets( $widgets_manager ) );
add_action( 'elementor/editor/after_enqueue_scripts', fn() => $this->enqueue_scripts() );
}
}
private function register_experiment() {
Plugin::$instance->experiments->add_feature( [
'name' => self::EXPERIMENT_NAME,
'title' => esc_html__( 'Atomic Widgets', 'elementor' ),
'description' => esc_html__( 'Enable atomic widgets.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_INACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_ALPHA,
] );
}
private function add_packages( $packages ) {
return array_merge( $packages, self::PACKAGES );
}
private function register_widgets( Widgets_Manager $widgets_manager ) {
$widgets_manager->register( new Atomic_Heading() );
$widgets_manager->register( new Atomic_Image() );
}
/**
* Enqueue the module scripts.
*
* @return void
*/
private function enqueue_scripts() {
wp_enqueue_script(
'elementor-atomic-widgets-editor',
$this->get_js_assets_url( 'atomic-widgets-editor' ),
[ 'elementor-editor' ],
ELEMENTOR_VERSION,
true
);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Schema;
use JsonSerializable;
class Atomic_Prop implements JsonSerializable {
private $default_value = null;
public static function make(): self {
return new self();
}
public function default( $default_value ): self {
$this->default_value = $default_value;
return $this;
}
public function get_default() {
return $this->default_value;
}
public function jsonSerialize(): array {
return [
'default' => $this->default_value,
];
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Widgets;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\Controls\Types\Textarea_Control;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Schema\Atomic_Prop;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Heading extends Atomic_Widget_Base {
public function get_icon() {
return 'eicon-t-letter';
}
public function get_title() {
return esc_html__( 'Atomic Heading', 'elementor' );
}
public function get_name() {
return 'a-heading';
}
protected function render() {
$settings = $this->get_atomic_settings();
// TODO: Move the validation/sanitization to the props schema constraints.
$escaped_tag = Utils::validate_html_tag( $settings['tag'] );
$escaped_title = esc_html( $settings['title'] );
$class = '';
if ( ! empty( $settings['classes'] ) ) {
$class = "class='" . esc_attr( $settings['classes'] ) . "'";
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "<$escaped_tag $class>$escaped_title</$escaped_tag>";
}
protected function define_atomic_controls(): array {
$tag_control = Select_Control::bind_to( 'tag' )
->set_label( esc_html__( 'Tag', 'elementor' ) )
->set_options( [
[
'value' => 'h1',
'label' => 'H1',
],
[
'value' => 'h2',
'label' => 'H2',
],
[
'value' => 'h3',
'label' => 'H3',
],
[
'value' => 'h4',
'label' => 'H4',
],
[
'value' => 'h5',
'label' => 'H5',
],
[
'value' => 'h6',
'label' => 'H6',
],
]);
$title_control = Textarea_Control::bind_to( 'title' )
->set_label( __( 'Title', 'elementor' ) )
->set_placeholder( __( 'Type your title here', 'elementor' ) );
$tag_and_title_section = Section::make()
->set_label( __( 'Content', 'elementor' ) )
->set_items( [
$tag_control,
$title_control,
]);
return [
$tag_and_title_section,
];
}
protected static function define_props_schema(): array {
return [
'classes' => Atomic_Prop::make(),
'tag' => Atomic_Prop::make()
->default( 'h2' ),
'title' => Atomic_Prop::make()
->default( __( 'Your Title Here', 'elementor' ) ),
];
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Widgets;
use Elementor\Utils;
use Elementor\Modules\AtomicWidgets\Controls\Section;
use Elementor\Modules\AtomicWidgets\Controls\Types\Select_Control;
use Elementor\Modules\AtomicWidgets\Base\Atomic_Widget_Base;
use Elementor\Modules\AtomicWidgets\Schema\Atomic_Prop;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Atomic_Image extends Atomic_Widget_Base {
public function get_icon() {
return 'eicon-image';
}
public function get_title() {
return esc_html__( 'Atomic Image', 'elementor' );
}
public function get_name() {
return 'a-image';
}
protected function render() {
$settings = $this->get_atomic_settings();
// TODO: Replace with actual URL prop
$image_url = $settings['url'];
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "<img src='" . esc_url( $image_url ) . "' />";
}
private function get_image_size_options() {
$wp_image_sizes = self::get_wp_image_sizes();
$image_sizes = [];
foreach ( $wp_image_sizes as $size_key => $size_attributes ) {
$control_title = ucwords( str_replace( '_', ' ', $size_key ) );
if ( is_array( $size_attributes ) ) {
$control_title .= sprintf( ' - %d*%d', $size_attributes['width'], $size_attributes['height'] );
}
$image_sizes[] = [
'label' => $control_title,
'value' => $size_key,
];
}
$image_sizes[] = [
'label' => esc_html__( 'Full', 'elementor' ),
'value' => 'full',
];
return $image_sizes;
}
private static function get_wp_image_sizes() {
$default_image_sizes = get_intermediate_image_sizes();
$additional_sizes = wp_get_additional_image_sizes();
$image_sizes = [];
foreach ( $default_image_sizes as $size ) {
$image_sizes[ $size ] = [
'width' => (int) get_option( $size . '_size_w' ),
'height' => (int) get_option( $size . '_size_h' ),
'crop' => (bool) get_option( $size . '_crop' ),
];
}
if ( $additional_sizes ) {
$image_sizes = array_merge( $image_sizes, $additional_sizes );
}
// /** This filter is documented in wp-admin/includes/media.php */
return apply_filters( 'image_size_names_choose', $image_sizes );
}
protected function define_atomic_controls(): array {
$options = $this->get_image_size_options();
$resolution_control = Select_Control::bind_to( 'image_size' )
->set_label( esc_html__( 'Image Resolution', 'elementor' ) )
->set_options( $options );
$content_section = Section::make()
->set_label( esc_html__( 'Content', 'elementor' ) )
->set_items( [
$resolution_control,
]);
return [
$content_section,
];
}
protected static function define_props_schema(): array {
return [
'image_size' => Atomic_Prop::make()
->default( 'large' ),
'url' => Atomic_Prop::make()
->default( Utils::get_placeholder_image_src() ),
];
}
}