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,253 @@
<?php
/**
* Image Select Field.
*
* @package ReduxFramework/Fields
* @author Dovy Paukstys & Kevin Provance (kprovance)
* @version 4.0.0
*/
defined( 'ABSPATH' ) || exit;
// Don't duplicate me!
if ( ! class_exists( 'Redux_Image_Select', false ) ) {
/**
* Main Redux_image_select class
*
* @since 1.0.0
*/
class Redux_Image_Select extends Redux_Field {
/**
* Set field defaults.
*/
public function set_defaults() {
$defaults = array(
'tiles' => false,
'mode' => 'background-image',
'presets' => false,
'options' => array(),
'width' => '',
'height' => '',
);
$this->field = wp_parse_args( $this->field, $defaults );
}
/**
* Field Render Function.
* Takes the vars and outputs the HTML for the field in the settings
*
* @since 1.0.0
* @access public
* @return void
*/
public function render() {
if ( ! empty( $this->field['options'] ) ) {
echo '<div class="redux-table-container">';
echo '<ul class="redux-image-select">';
$x = 1;
foreach ( $this->field['options'] as $k => $v ) {
if ( ! is_array( $v ) ) {
$v = array( 'img' => $v );
}
if ( ! isset( $v['title'] ) ) {
$v['title'] = '';
}
if ( ! isset( $v['alt'] ) ) {
$v['alt'] = $v['title'];
}
if ( ! isset( $v['class'] ) ) {
$v['class'] = '';
}
$style = '';
if ( ! empty( $this->field['width'] ) ) {
$style .= 'width: ' . $this->field['width'];
if ( is_numeric( $this->field['width'] ) ) {
$style .= 'px';
}
$style .= ';';
} else {
$style .= ' width: 100%; ';
}
if ( ! empty( $this->field['height'] ) ) {
$style .= 'height: ' . $this->field['height'];
if ( is_numeric( $this->field['height'] ) ) {
$style .= 'px';
}
$style .= ';';
}
$the_value = $k;
if ( ! empty( $this->field['tiles'] ) && true === (bool) $this->field['tiles'] ) {
$the_value = $v['img'];
}
$selected = ( '' !== checked( $this->value, $the_value, false ) ) ? ' redux-image-select-selected' : '';
$presets = '';
$is_preset = false;
$this->field['class'] = trim( str_replace( 'no-update', '', $this->field['class'] ) );
$this->field['class'] .= ' no-update ';
if ( isset( $this->field['presets'] ) && false !== $this->field['presets'] ) {
$this->field['class'] = trim( $this->field['class'] );
if ( ! isset( $v['presets'] ) ) {
$v['presets'] = array();
}
if ( ! is_array( $v['presets'] ) ) {
$v['presets'] = json_decode( $v['presets'], true );
}
// Only highlight the preset if it's the same.
if ( $selected ) {
if ( empty( $v['presets'] ) ) {
$selected = false;
} else {
foreach ( $v['presets'] as $pk => $pv ) {
if ( isset( $v['merge'] ) && false !== $v['merge'] ) {
if ( ( true === $v['merge'] || in_array( $pk, $v['merge'], true ) ) && is_array( $this->parent->options[ $pk ] ) ) {
$pv = array_merge( $this->parent->options[ $pk ], $pv );
}
}
if ( empty( $pv ) && isset( $this->parent->options[ $pk ] ) && ! empty( $this->parent->options[ $pk ] ) ) {
$selected = false;
} elseif ( ! empty( $pv ) && ! isset( $this->parent->options[ $pk ] ) ) {
$selected = false;
}
if ( ! $selected ) { // We're still not using the same preset. Let's unset that shall we?
$this->value = '';
break;
}
}
}
}
$v['presets']['redux-backup'] = 1;
$presets = ' data-presets="' . esc_attr( htmlspecialchars( wp_json_encode( $v['presets'] ), ENT_QUOTES ) ) . '"';
$is_preset = true;
$this->field['class'] = trim( $this->field['class'] ) . ' redux-presets';
}
$is_preset_class = $is_preset ? '-preset-' : ' ';
$merge = '';
if ( isset( $v['merge'] ) && false !== $v['merge'] ) {
$merge = is_array( $v['merge'] ) ? implode( '|', $v['merge'] ) : 'true';
$merge = ' data-merge="' . esc_attr( htmlspecialchars( $merge, ENT_QUOTES ) ) . '"';
}
echo '<li class="redux-image-select">';
echo '<label class="' . esc_attr( $selected ) . ' redux-image-select' . esc_attr( $is_preset_class ) . esc_attr( $this->field['id'] . '_' . $x ) . '" for="' . esc_attr( $this->field['id'] . '_' . ( array_search( $k, array_keys( $this->field['options'] ), true ) + 1 ) ) . '">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<input type="radio" class="' . esc_attr( $this->field['class'] ) . '" id="' . esc_attr( $this->field['id'] . '_' . ( array_search( $k, array_keys( $this->field['options'] ), true ) + 1 ) ) . '" name="' . esc_attr( $this->field['name'] . $this->field['name_suffix'] ) . '" value="' . esc_attr( $the_value ) . '" ' . checked( $this->value, $the_value, false ) . $presets . $merge . '/>';
if ( ! empty( $this->field['tiles'] ) && true === $this->field['tiles'] ) {
echo '<span class="tiles ' . esc_attr( $v['class'] ) . '" style="background-image: url(' . esc_url( $v['img'] ) . ');" rel="' . esc_url( $v['img'] ) . '"">&nbsp;</span>';
} else {
echo '<img src="' . esc_url( $v['img'] ) . '" title="' . esc_attr( $v['alt'] ) . '" alt="' . esc_attr( $v['alt'] ) . '" class="' . esc_attr( $v['class'] ) . '" style="' . esc_attr( $style ) . '"' . esc_attr( $presets ) . esc_attr( $merge ) . ' />';
}
if ( '' !== $v['title'] ) {
echo '<br /><span>' . wp_kses_post( ( $v['title'] ) ) . '</span>';
}
echo '</label>';
echo '</li>';
++$x;
}
echo '</ul>';
echo '</div>';
}
}
/**
* Enqueue Function.
* If this field requires any scripts, or css define this function and register/enqueue the scripts/css
*
* @since 1.0.0
* @access public
* @return void
*/
public function enqueue() {
wp_enqueue_script(
'redux-field-image-select',
Redux_Core::$url . 'inc/fields/image_select/redux-image-select' . Redux_Functions::is_min() . '.js',
array( 'jquery', 'redux-js' ),
$this->timestamp,
true
);
if ( $this->parent->args['dev_mode'] ) {
wp_enqueue_style(
'redux-field-image-select',
Redux_Core::$url . 'inc/fields/image_select/redux-image-select.css',
array(),
$this->timestamp
);
}
}
/**
* Compile CSS data for output.
*
* @param string $data css string.
*
* @return string
*/
public function css_style( $data ): string {
$css = '';
$output = '';
$mode = ( isset( $this->field['mode'] ) && ! empty( $this->field['mode'] ) ? $this->field['mode'] : 'background-image' );
if ( ! empty( $data ) && ! is_array( $data ) ) {
switch ( $mode ) {
case 'background-image':
if ( isset( $this->field['tiles'] ) && true === (bool) $this->field['tiles'] ) {
$img = $data;
} else {
$img = $this->field['options'][ $data ]['img'] ?? '';
}
if ( '' !== $img ) {
$output = "background-image: url('" . esc_url( $img ) . "');";
}
break;
default:
$output = $mode . ': ' . $data . ';';
}
}
$css .= $output;
return $css;
}
}
}
class_alias( 'Redux_Image_Select', 'ReduxFramework_Image_Select' );

View File

@@ -0,0 +1,8 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
_deprecated_file( 'field_image_select.php', '4.3', 'class-redux-image-select.php', 'This file has been renamed and is no longer used in Redux 4. Please change any references to it as it will be removed in future versions of Redux.' );

View File

@@ -0,0 +1,8 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
echo null;

View File

@@ -0,0 +1,25 @@
.redux-container-image_select .redux-table-container { display: table; table-layout: fixed; width: 100%; }
.redux-container-image_select .redux-image-select { margin: 0 !important; }
.redux-container-image_select .redux-image-select .tiles { display: block; background-color: #fff; background-repeat: repeat; width: 40px; height: 40px; }
.redux-container-image_select .redux-image-select img, .redux-container-image_select .redux-image-select .tiles { border-color: #d9d9d9; }
.redux-container-image_select .redux-image-select li:last-child { margin-bottom: 0; }
.redux-container-image_select .redux-image-select input[type="radio"] { display: none; }
.redux-container-image_select .redux-image-select-presets img { width: 100%; }
.redux-container-image_select ul.redux-image-select li { margin: 0 10px 3px 10px; display: inline-block; padding: 2px 2px 2px 0; }
.redux-container-image_select .redux-image-select-selected { background-color: #f9f9f9; }
.redux-container-image_select .redux-image-select img, .redux-container-image_select .redux-image-select-selected img, .redux-container-image_select .redux-image-select .tiles, .redux-container-image_select .redux-image-select-selected .tiles { border-width: 4px; border-style: solid; }
.redux-container-image_select .redux-image-select-selected .tiles, .redux-container-image_select .redux-image-select-selected .tiles { border-color: #7a7a7a; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVkdXgtaW1hZ2Utc2VsZWN0LmNzcyIsInNvdXJjZXMiOlsicmVkdXgtaW1hZ2Utc2VsZWN0LnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsQUFDSSw2QkFEeUIsQ0FDekIsc0JBQXNCLENBQUMsRUFDbkIsT0FBTyxFQUFFLEtBQUssRUFDZCxZQUFZLEVBQUUsS0FBSyxFQUNuQixLQUFLLEVBQUUsSUFBSSxHQUNkOztBQUxMLEFBT0ksNkJBUHlCLENBT3pCLG1CQUFtQixDQUFDLEVBQ2hCLE1BQU0sRUFBRSxZQUFZLEdBc0J2Qjs7QUE5QkwsQUFVUSw2QkFWcUIsQ0FPekIsbUJBQW1CLENBR2YsTUFBTSxDQUFDLEVBQ0gsT0FBTyxFQUFFLEtBQUssRUFDZCxnQkFBZ0IsRUFBRSxJQUFJLEVBQ3RCLGlCQUFpQixFQUFFLE1BQU0sRUFDekIsS0FBSyxFQUFFLElBQUksRUFDWCxNQUFNLEVBQUUsSUFBSSxHQUNmOztBQWhCVCxBQWtCUSw2QkFsQnFCLENBT3pCLG1CQUFtQixDQVdmLEdBQUcsRUFsQlgsNkJBQTZCLENBT3pCLG1CQUFtQixDQVlmLE1BQU0sQ0FBQyxFQUNILFlBQVksRUFBRSxPQUFPLEdBQ3hCOztBQXJCVCxBQXVCUSw2QkF2QnFCLENBT3pCLG1CQUFtQixDQWdCZixFQUFFLEFBQUEsV0FBVyxDQUFDLEVBQ1YsYUFBYSxFQUFFLENBQUMsR0FDbkI7O0FBekJULEFBMkJRLDZCQTNCcUIsQ0FPekIsbUJBQW1CLENBb0JmLEtBQUssQ0FBQSxBQUFBLElBQUMsQ0FBSyxPQUFPLEFBQVosRUFBYyxFQUNoQixPQUFPLEVBQUUsSUFBSSxHQUNoQjs7QUE3QlQsQUFnQ0ksNkJBaEN5QixDQWdDekIsMkJBQTJCLENBQUMsR0FBRyxDQUFDLEVBQzVCLEtBQUssRUFBRSxJQUFJLEdBQ2Q7O0FBbENMLEFBb0NJLDZCQXBDeUIsQ0FvQ3pCLEVBQUUsQUFBQSxtQkFBbUIsQ0FBQyxFQUFFLENBQUMsRUFDckIsTUFBTSxFQUFFLGVBQWUsRUFDdkIsT0FBTyxFQUFFLFlBQVksRUFDckIsT0FBTyxFQUFFLGFBQWEsR0FDekI7O0FBeENMLEFBMENJLDZCQTFDeUIsQ0EwQ3pCLDRCQUE0QixDQUFDLEVBQ3pCLGdCQUFnQixFQUFFLE9BQU8sR0FDNUI7O0FBNUNMLEFBOENJLDZCQTlDeUIsQ0E4Q3pCLG1CQUFtQixDQUFDLEdBQUcsRUE5QzNCLDZCQUE2QixDQStDekIsNEJBQTRCLENBQUMsR0FBRyxFQS9DcEMsNkJBQTZCLENBZ0R6QixtQkFBbUIsQ0FBQyxNQUFNLEVBaEQ5Qiw2QkFBNkIsQ0FpRHpCLDRCQUE0QixDQUFDLE1BQU0sQ0FBQyxFQUNoQyxZQUFZLEVBQUUsR0FBRyxFQUNqQixZQUFZLEVBQUUsS0FBSyxHQUN0Qjs7QUFwREwsQUF3RFEsNkJBeERxQixDQXNEekIsNEJBQTRCLENBRXhCLE1BQU0sRUF4RGQsNkJBQTZCLENBdUR6Qiw0QkFBNEIsQ0FDeEIsTUFBTSxDQUFDLEVBQ0gsWUFBWSxFQUFFLE9BQU8sR0FDeEIifQ== */
/*# sourceMappingURL=redux-image-select.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,117 @@
/* global redux, redux_change, jQuery */
(function( $ ) {
'use strict';
redux.field_objects = redux.field_objects || {};
redux.field_objects.image_select = redux.field_objects.image_select || {};
redux.field_objects.image_select.init = function( selector ) {
selector = $.redux.getSelector( selector, 'image_select' );
$( selector ).each(
function() {
var el = $( this );
var parent = el;
if ( ! el.hasClass( 'redux-field-container' ) ) {
parent = el.parents( '.redux-field-container:first' );
}
if ( parent.is( ':hidden' ) ) {
return;
}
if ( parent.hasClass( 'redux-field-init' ) ) {
parent.removeClass( 'redux-field-init' );
} else {
return;
}
// On label click, change the input and class.
el.find( '.redux-image-select label img, .redux-image-select label .tiles' ).on(
'click',
function( e ) {
var presets;
var data;
var merge;
var importCodeValue;
var id = $( this ).closest( 'label' ).attr( 'for' );
$( this ).parents( 'fieldset:first' ).find( '.redux-image-select-selected' )
.removeClass( 'redux-image-select-selected' ).find( 'input[type="radio"]' ).prop( 'checked', false );
$( this ).closest( 'label' ).find( 'input[type="radio"]' ).prop( 'checked' );
if ( $( this ).closest( 'label' ).hasClass( 'redux-image-select-preset-' + id ) ) { // If they clicked on a preset, import!
e.preventDefault();
presets = $( this ).closest( 'label' ).find( 'input' );
data = presets.data( 'presets' );
merge = presets.data( 'merge' );
if ( undefined !== merge && null !== merge ) {
if ( 'string' === typeof( merge ) ) {
merge = merge.split( '|' );
}
$.each(
data,
function( index ) {
if ( 'object' === typeof( redux.optName.options[index] ) && (
true === merge || -1 !== $.inArray( index, merge ) )
) {
data[index] = $.extend( redux.optName.options[index], data[index] );
}
}
);
}
if ( undefined !== presets && null !== presets ) {
el.find( 'label[for="' + id + '"]' ).addClass( 'redux-image-select-selected' )
.find( 'input[type="radio"]' ).attr( 'checked', true );
window.onbeforeunload = null;
importCodeValue = $( 'textarea[name="' + redux.optName.args.opt_name + '[import_code]"' );
if ( 0 === importCodeValue.length ) {
$( this ).append( '<textarea id="import-code-value" style="display:none;" name="' + redux.optName.args.opt_name + '[import_code]">' + JSON.stringify( data ) + '</textarea>' );
} else {
importCodeValue.val( JSON.stringify( data ) );
}
if ( 0 !== $( '#publishing-action #publish' ).length ) {
$( '#publish' ).trigger( 'click' );
} else {
$( '#redux-import' ).trigger( 'click' );
}
}
return false;
} else {
el.find( 'label[for="' + id + '"]' ).addClass( 'redux-image-select-selected' ).find( 'input[type="radio"]' ).prop( 'checked', true ).trigger( 'change' );
redux_change( $( this ).closest( 'label' ).find( 'input[type="radio"]' ) );
}
}
);
// Used to display a full image preview of a tile/pattern.
el.find( '.tiles' ).qtip(
{
content: {
text: function() {
return '<img src="' + $( this ).attr( 'rel' ) + '" style="max-width:150px;" alt=" />';
}
}, style: 'qtip-tipsy', position: {
my: 'top center', // Position my top left...
at: 'bottom center' // At the bottom right of...
}
}
);
}
);
};
})( jQuery );

View File

@@ -0,0 +1 @@
!function(a){"use strict";redux.field_objects=redux.field_objects||{},redux.field_objects.image_select=redux.field_objects.image_select||{},redux.field_objects.image_select.init=function(e){e=a.redux.getSelector(e,"image_select"),a(e).each(function(){var l=a(this),e=l;(e=l.hasClass("redux-field-container")?e:l.parents(".redux-field-container:first")).is(":hidden")||e.hasClass("redux-field-init")&&(e.removeClass("redux-field-init"),l.find(".redux-image-select label img, .redux-image-select label .tiles").on("click",function(e){var t,i,s=a(this).closest("label").attr("for");if(a(this).parents("fieldset:first").find(".redux-image-select-selected").removeClass("redux-image-select-selected").find('input[type="radio"]').prop("checked",!1),a(this).closest("label").find('input[type="radio"]').prop("checked"),a(this).closest("label").hasClass("redux-image-select-preset-"+s))return e.preventDefault(),e=a(this).closest("label").find("input"),t=e.data("presets"),null!=(i=e.data("merge"))&&("string"==typeof i&&(i=i.split("|")),a.each(t,function(e){"object"!=typeof redux.optName.options[e]||!0!==i&&-1===a.inArray(e,i)||(t[e]=a.extend(redux.optName.options[e],t[e]))})),null!=e&&(l.find('label[for="'+s+'"]').addClass("redux-image-select-selected").find('input[type="radio"]').attr("checked",!0),window.onbeforeunload=null,0===(e=a('textarea[name="'+redux.optName.args.opt_name+'[import_code]"')).length?a(this).append('<textarea id="import-code-value" style="display:none;" name="'+redux.optName.args.opt_name+'[import_code]">'+JSON.stringify(t)+"</textarea>"):e.val(JSON.stringify(t)),(0!==a("#publishing-action #publish").length?a("#publish"):a("#redux-import")).trigger("click")),!1;l.find('label[for="'+s+'"]').addClass("redux-image-select-selected").find('input[type="radio"]').prop("checked",!0).trigger("change"),redux_change(a(this).closest("label").find('input[type="radio"]'))}),l.find(".tiles").qtip({content:{text:function(){return'<img src="'+a(this).attr("rel")+'" style="max-width:150px;" alt=" />'}},style:"qtip-tipsy",position:{my:"top center",at:"bottom center"}}))})}}(jQuery);

View File

@@ -0,0 +1,62 @@
.redux-container-image_select {
.redux-table-container {
display: table;
table-layout: fixed;
width: 100%;
}
.redux-image-select {
margin: 0 !important;
.tiles {
display: block;
background-color: #fff;
background-repeat: repeat;
width: 40px;
height: 40px;
}
img,
.tiles {
border-color: #d9d9d9;
}
li:last-child {
margin-bottom: 0;
}
input[type="radio"] {
display: none;
}
}
.redux-image-select-presets img {
width: 100%;
}
ul.redux-image-select li {
margin: 0 10px 3px 10px;
display: inline-block;
padding: 2px 2px 2px 0;
}
.redux-image-select-selected {
background-color: #f9f9f9;
}
.redux-image-select img,
.redux-image-select-selected img,
.redux-image-select .tiles,
.redux-image-select-selected .tiles {
border-width: 4px;
border-style: solid;
}
.redux-image-select-selected,
.redux-image-select-selected {
.tiles {
border-color: #7a7a7a;
}
}
}