I’m back to bring you the next part of this series. In this part, we are adding a booking form to the Room’s page which shows all the room’s information. This form allows your customers to submit a booking from frontend easily.

This booking form has some different things from the one we created in part 2, mainly the functionality of fields. Let’s see how.

Before Getting Started

We are going to allow visitors to choose a date without the locked or passed ones in the calendar. So, we need a Datepicker library in addition to the plugins I mentioned in part 1. This is a Javascript library which will help us to do this task.

I chose the Jquery UI Datepicker. There is no need to import this library to your site because Meta Box also uses it for the Date fields. It’s quite convenient. Just read it documentations to learn more about its callbacks and parameters in use.

Now, here we go to finish your booking page.

Show the Booking Form in the Room’s Page for Customer Using

We created a form for internal booking in part 2, so we will get that form once again to show in the room’s page. Then, add some additional configurations and functionalities to the form to meet the customer’s routine.

However, we just show the form in this step. Any further actions will be done later.

Step 1: Get the Shortcode of the Field Group which Show the Booking Form

In the admin dashboard, go to the Meta Box menu > Custom Fields to view all the available field groups. There will be a shortcode beside each one. Choose a shortcode of the field group you need and copy it.

Get the Shortcode of the Field Group which Show the Booking Form

Step 2: Put the Shortcode into the Room’s Page

If you are using a Page Builder plugin, it’s so easy to put the shortcode there.

Suppose that you are using Elementor. Just add a widget for the shortcode type then paste the above shortcode to the content of the widget.

Put the Shortcode into the Room’s Page

Otherwise, if you’re not using any page builder plugin, tackle in your code a little bit as bellow:

  • Create a new file named single-room.php in your theme folder. In there, “room” is the slug of the post type used for the room’s page.
  • Copy all the code in the single.php file then paste to single-room.php.
  • Use the below do_shortcode() function of WordPress and add it to the place where you want to show the booking form.
<?php echo do_shortcode(‘[mb_frontend_form id='booking-fields' post_fields='title,content']’); ?>

In there, [mb_frontend_form id='booking-fields' post_fields='title,content'] is the shortcode of the field group which I copied in step 1.

Put the do_shortcode() into the place you want
Put the do_shortcode() into the place you want

Then, you will see the booking form created in part 2 shown on the room’s page.

the booking form created in part 2

Your website visitor now can fill this form.

Disable the Unnecessary Fields Using CSS

Your customer probably does not care about some information such as order number, booking date, person in charge, etc. So we should disable them in the form.

In the edit page of each field group (Meta Box > Custom Fields), go to the custom field which you want to disable. Eg. the Order Number field. Move to the Appearance tab and add “hide-frontend” to the Custom CSS Class box.

Add a CSS Class for a custom field
Add a CSS Class for a custom field

Do likewise with the same CSS Class “hide-frontend” for the other fields. After that, go to the file style.css in your theme folder or Customizer screen (Appearance > Customize) and put this CSS code there.

.hide-frontend{
    display: none!important;
}

I want to disable the Add Room button as well, so I also add this CSS code:

.rwmb-button.add-clone {
    display: none!important;
}

Both fields and the Add Room button are disable now.

Settle the Functions of Check-in and Check-out Date Fields

We will use JS for this, so we create a file named custom.js in your theme folder to process JS for the Room’s page only. This will help to speed up the process and avoid unexpected errors.

Paste this code to the functions.php file:

function enqueue_script() {
    if (is_singular('room')) {
        wp_enqueue_script('custom-script', get_template_directory_uri().'/js/custom.js');
    }
}
add_action( 'wp_enqueue_scripts', ‘enqueue_script’);

In there:

  • is_singular(‘room’): to check if the current page is the room’s page which has the post type’s slug is room. This stipulates that the custom.js file is used for the rom’s page only.
  • wp_enqueue_script: to import the custom.js file.
  • get_template_directory_url(): to return the theme’s url

Create a Range for Check-in and Check-out Date

Check-in date always is before the check-out date, so we need to limit their range. When a customer chooses a check-in date, all the dates before it will be locked and can not be chosen.

I use Jquery UI Datepicker here with the following code and add it to the custom.js file.

jQuery( function($) {
    var dateFormat = "yy-mm-dd",
        from = $( "#group_booking_check_in" )
        .datepicker({
        defaultDate: "+1w",
        changeMonth: true,
        })
        .on( "change", function() {
        console.log('helloo');
        to.datepicker( "option", "minDate", getDate( this ) );
        }),
        to = $( "#group_booking_check_out" ).datepicker({
        defaultDate: "+1w",
        changeMonth: true,
        })
        .on( "change", function() {
        from.datepicker( "option", "maxDate", getDate( this ) );
        });

    function getDate( element ) {
        var date;
        try {
        date = $.datepicker.parseDate( dateFormat, element.value );
        } catch( error ) {
        date = null;
        }
        return date;
    }
} );

In there, #group_booking_check_in and #group_booking_check_out are the IDs of the check-in date and check-out date fields in turn. You should replace these IDs with your ones.

Get the field’s ID
Get the field’s ID

You will get this:

Create a Range for Check-in and Check-out Date

Check the Vacancy and Lock Those Days are Out of Room

This is the key of this series. I tried many ways and finally found out the most simple and optimized one.

Step 1: Create an Option to Store the Booking Dates

Whenever a booking is generated, the values of the booking dates will be input to this option with the corresponding room’s ID.

Option’s value is an array as below:

array(
    room_1 => array(
        booking_1 => array(
            key_1 => array(date1, date2),
            key_2 => array(date2, date3),
        )
        booking_2 => array(
            key_1 => array(date1, date2),
            key_2 => array(date2, date3),
        )
    ),
    room_2 => array(
        booking_1 => array(
            key_1 => array(date1, date2),
            key_2 => array(date2, date3),
        )
    )
)

In there:

  • room_1, room_2, … are rooms’ IDs
  • booking_1, booking_2, … are bookings’ IDs
  • key_1, key_2, … are the number order of the cloned group
  • date1, date2, … are the booking dates as the booking order

Next, add this below code to the functions.php to create an option named rwmb_bookings with the initial value is an empty array.

if( !get_option('rwmb_bookings') ){
    add_option('rwmb_bookings', array());
}

You may dive in more ways to create an option in WordPress here.

Step 2: Input Values into Option Whenever a Booking is Generated

Add this code to the functions.php file.

add_action( 'rwmb_booking-fields_after_save_post', 'update_bookings_date' );
function update_bookings_date( $post_id ) {
    $bookings = get_post_meta( $post_id, 'group_booking', true ); // Get the value of the group_booking field then put it into $booking
    if (empty($bookings) && !is_array($bookings)) return;
    $option = get_option('rwmb_bookings');
        foreach ($bookings as $key => $booking) { // Run a loop for $booking because this is a field group
            $room = $booking['room']; // $room is the parameter holds the room’s id
            $begin = new DateTime( $booking['check_in'] ); // $begin is the begin date, means the check-in date as well
            $end = new DateTime( $booking['check_out'] ); // $end is the end date, means the check-out date as well
            $interval = new DateInterval('P1D');
            $daterange = new DatePeriod($begin, $interval ,$end); // The DatePeriod function returns an array containing all the dates from the begin to the end date, excluding the end date
            $dates_booking = array();
            foreach($daterange as $date){
                array_push($dates_booking, $date->format("Y-m-d")); // Push the values to the $dates_booking parameter
            }
            $option[$room][$post_id][$key] = $dates_booking; // Assign the value to the $option parameter
        }
        update_option('rwmb_bookings', $option); // Update the 'rwmb_bookings' option
}

In this code, I used the rwmb_{$field_id}_after_save_post hook of Meta Box to push values to the option parameter. There, my  $field_id  is booking-fields. Whereby, whenever a post, which contain the field group with the ID is booking-fields, is saved (means has a new booking order), this hook will execute these following actions:

  • Get values of the check-in and check-out dates
  • Use DatePeriod functions of PHP to expose all the dates between these dates.
  • Push the above dates (excluding the check-out date) into the option parametar with the corresponding room_id.

Step 3: Check Those Days are Out of Room and the Vacancy from the Option Parameter

Still in the functions.php file, add this code:

function amounts_by_date($room_id){
    $bookings = get_option('rwmb_bookings')[$room_id];
    $dates = array();
    foreach ($bookings as $booking) {
        foreach ($booking as $value) {
            foreach ($value as $k ) {
                array_push($dates, $k);
            }
        }
    }
    return array_count_values($dates);
}

By this, we got the room_id of the room which your customer is viewing, then compare it with the option parameter exporting an array containing dates has booking along with the number of booked rooms of that room type. For example:

Array (
    [2019-12-18] => 1
    [2019-12-19] => 2
    [2019-12-20] => 1
    [2019-12-21] => 1
    [2020-01-31] => 1
    [2020-02-01] => 1
)

As the values in this array, there are 6 days which has booking for the viewing room type. There are two rooms booked in 2019-12-19, and only one room in the 5 other days. So far, you do not know which day has the vacant room.

In the event that this room type has 2 rooms. So that, you are out of the Family room in 2019-12-19 only which needs to be locked on the calendar.

That’s why you need to check the quantity of rooms of the viewing room type to check the exact date is out of room by using this:

$quantity = rwmb_meta( 'quantity', $room_id);

Ok, now rename the above function from amounts_by_date($room_id) to be dates_disable() as the following:

function dates_disable($room_id){
    $bookings = get_option('rwmb_bookings')[$room_id];
    if (empty($bookings) && !is_array($bookings)) return;
    $dates = array();
    $disable = array();
    foreach ($bookings as $booking) {
        foreach ($booking as $value) {
            foreach ($value as $k ) {
                array_push($dates, $k);
            }
        }
    }
    $dates = array_count_values($dates);
    $quantity = rwmb_meta( 'quantity', $room_id);
    foreach ($dates as $key => $date) {
        if ($date >= $quantity) {
            array_push($disable, $key);
        }
    }
    return $disable;
}

This function will return only the dates we need to lock. Thus, when you want to use this date, just call the parameter:

$disable = dates_disable($room_id);

Instead of putting them all into a function as I did, you can use a parameter assigned by the first function then create a new function to check.

Step 4: Lock Those Dates in the Check-in and Check-out Date field using Javascript

We will assign a PHP parameter (dates_disable) to a JS’s one to disable the dates by jQuery Datepicker UI.

Paste this following code to the function used to import custom.js, under the wp_enqueue_script()... line.

wp_localize_script( 'custom-script', 'disable_dates', json_encode(dates_disable(get_the_ID())));
Transmit the “dates_disable" parameter to JS
Transmit the “dates_disable” parameter to JS

To check if you did it right, do console.log(disable_dates) then open the console tab on the room’s page. Remember to create some booking first, then you can check it more perfectly.

Next, add this code to the custom.js file to lock the dates are out of room.

from.add(to).datepicker({
    beforeShowDay: function(date){
        var string = jQuery.datepicker.formatDate('yy-mm-dd', date);
        return [ dates_disable.indexOf(string) == -1 ]
    }
});

In there:

  • From, to: are to parameters used in the step of creating the date range.
  • Dates_disable is an array store which days we want to lock

You can see that the date 2019-12-19 is locked now:

Lock Those Dates in the Check-in and Check-out Date field using Javascript

Step 5: Optimize to the Option Parameter

As you can see, this option parameter contains unlimited values of the booking dates. This will weight your database and slow down the checking process.

For this reason, you should optimize it in addition to exclude all the days before the current date. It will reduce the number of values in your option parameter.

We’ll use both two following functions:

function array_filter_recursive ($data) {
    $original = $data;
    $data = array_filter($data);
    $data = array_map(function ($e) {
    return is_array($e) ? array_filter_recursive($e) : $e;
    }, $data);
    return $original === $data ? $data : array_filter_recursive($data);
}
function optimal_bookings_option() {
    $bookings = get_option('rwmb_bookings');
    if (empty($bookings) && !is_array($bookings)) return;
    $today = date("Y-m-d");
    foreach ($bookings as $key_1 => $bk_1) {
        foreach ($bk_1 as $key_2 => $bk_2) {
            foreach ($bk_2 as $key_3 => $bk_3 ) {
                foreach ($bk_3 as $key_4 => $bk_4) {
                    if ($bk_4 > $today) {
                unset($bookings[$key_1][$key_2][$key_3][$key_4]);
                    }
                }

            }
        }
    }
    $bookings = array_filter_recursive($bookings);
    update_option('rwmb_bookings', $bookings);
}

add_action( 'init', 'optimal_bookings_option' );

In there:

  • optimal_bookings_option() helps to compare the booking date with the current date and remove the past date in the Option.
  • array_filter_recursive() helps to filter and remove all the empty arrays and empty values.

I decided to use array_filter_recursive() because right after removing some booking date, I got some empty values as below:

array(2) {

    [451]=> array(2) {
        [0]=> array(2) { }

        [2]=> array(3) {}
    }
    [440]=> array(1) {
        [0]=> array(2) {
            [0]=> string(10) "2020-01-31"
            [1]=> string(10) "2020-02-01"
        }
    }
}

The array_filter_recursive() function helps me to remove the two empty arrays in the array [451]. I found this technique on php.net, you can refer to it.

Add Conditions for Dates in Booking

Although we locked some dates, it is tackled in the display. If your customer is a coder, he absolutely can hack it to choose a locked date.

So, we should add some conditions to your booking date to avoid unexpected errors:

  • The check-in and check-out dates must not coincide with the locked dates.
  • The check-in date must be after the current date.

The valid booking must satisfy both the above conditions.

I use rwmb_frontend_validate to check these above conditions.

add_filter( 'rwmb_frontend_validate', function( $validate, $config ) {
    if ( 'booking-fields' !== $config['id'] ) {
        return $validate;
    }
    $disable_dates = dates_disable(get_the_ID());
        $checkin = date("Y-m-d", strtotime($_POST['group_booking_check_in']));
        $checkout = date("Y-m-d", strtotime($_POST['group_booking_check_out']));

    if ( false !== array_search($checkin, $disable_dates)) {
        $validate = false;
    } else {
        update_post_meta($config['post_id'], 'group_booking_room', get_the_ID());
    }
    return $validate;

}, 10, 2 );

By this, I checked if the booking is valid. If not, we should notify your customer that they have booked unsuccessfully.

Final Words

You have finished all the booking pages and forms for both internal and external use already. We will continue to the next post with the booking management page. This will help the hotel owner have an overall view about the stock. Wait for me in the next part.

In addition, for your quick reference, these are all the source code which I made so far.

  • Full source code of the custom.js file:
jQuery( function($) {
setTimeout(function(){
datepicker_reinstall();
console.log(disable_dates);
}, 1000);
function datepicker_reinstall(){
var dateFormat = "yy-mm-dd",
from = $( "#group_booking_check_in" ),
to = $( "#group_booking_check_out" );
from.add(to).datepicker('destroy');
from.datepicker({
minDate: 0,
defaultDate: "+1w",
beforeShowDay: function(date){
var string = jQuery.datepicker.formatDate('yy-mm-dd', date);
return [ disable_dates.indexOf(string) == 1 ]
}
})
.on( "change", function() {
to.datepicker( "option", "minDate", getDate( this ) );
});
to.datepicker({
defaultDate: "+1w",
beforeShowDay: function(date){
var string = jQuery.datepicker.formatDate('yy-mm-dd', date);
return [ disable_dates.indexOf(string) == 1 ]
}
})
.on( "change", function() {
from.datepicker( "option", "maxDate", getDate( this ) );
});
function getDate( element ) {
var date;
try {
date = $.datepicker.parseDate( dateFormat, element.value );
} catch( error ) {
date = null;
}
return date;
}
}
} );
view raw
custom.js
hosted with ❤ by GitHub
  • Full source code of the single-room.php file:
<?php get_header(); ?>
<div id="primary" class="content-area">
<main id="main" class="site-main">
<?php
while ( have_posts() ) : the_post();
get_template_part( 'template-parts/content', 'single' );
the_post_navigation();
echo do_shortcode("[mb_frontend_form id='booking-fields' post_fields='title,content']");
if ( comments_open() || get_comments_number() ) :
comments_template();
endif;
endwhile;
?>
</main>
</div>
<?php get_footer(); ?>
view raw
single-room.php
hosted with ❤ by GitHub
  • Full source code of the functions.php file:
if ( ! get_option( 'rwmb_bookings' ) ) {
add_option( 'rwmb_bookings', array() );
}
add_action( 'rwmb_booking-fields_after_save_post', 'update_bookings_date' );
function update_bookings_date( $post_id ) {
$bookings = get_post_meta( $post_id, 'group_booking', true );
if ( empty( $bookings ) || ! is_array( $bookings ) ) {
return;
}
$option = get_option( 'rwmb_bookings' );
foreach ( $bookings as $key => $booking ) {
$room = $booking['room'];
$begin = new DateTime( $booking['check_in'] );
$end = new DateTime( $booking['check_out'] );
$interval = new DateInterval('P1D');
$daterange = new DatePeriod($begin, $interval ,$end);
$dates_booking = array();
foreach($daterange as $date){
array_push($dates_booking, $date->format("Y-m-d"));
}
$option[$room][$post_id][$key] = $dates_booking;
}
update_option('rwmb_bookings', $option);
}
function dates_disable($room_id){
$bookings = get_option('rwmb_bookings')[$room_id];
if (empty($bookings) && !is_array($bookings)) return;
$dates = array();
$disable = array();
foreach ($bookings as $booking) {
foreach ($booking as $value) {
foreach ($value as $k ) {
array_push($dates, $k);
}
}
}
$dates = array_count_values($dates);
$quantity = rwmb_meta( 'quantity', $room_id);
foreach ($dates as $key => $date) {
if ($date >= $quantity) {
array_push($disable, $key);
}
}
return $disable;
}
function enqueue_script() {
if (is_singular('room')) {
wp_enqueue_script('custom-script', get_template_directory_uri().'/js/custom.js', array( 'jquery' ));
wp_localize_script( 'custom-script', 'ajaxurl', admin_url('admin-ajax.php'));
wp_localize_script( 'custom-script', 'disable_dates', json_encode(dates_disable(get_the_ID())));
}
}
add_action( 'wp_enqueue_scripts', 'enqueue_script');
/**
* Remove expired date in variable option
*/
function array_filter_recursive ($data) {
$original = $data;
$data = array_filter($data);
$data = array_map(function ($e) {
return is_array($e) ? array_filter_recursive($e) : $e;
}, $data);
return $original === $data ? $data : array_filter_recursive($data);
}
function optimal_bookings_option() {
$bookings = get_option('rwmb_bookings');
$today = date("Y-m-d");
foreach ($bookings as $key_1 => $bk_1) {
foreach ($bk_1 as $key_2 => $bk_2) {
foreach ($bk_2 as $key_3 => $bk_3 ) {
foreach ($bk_3 as $key_4 => $bk_4) {
if ($bk_4 < $today) {
unset($bookings[$key_1][$key_2][$key_3][$key_4]);
}
}
}
}
}
$bookings = array_filter_recursive($bookings);
update_option('rwmb_bookings', $bookings);
}
add_action( 'init', 'optimal_bookings_option' );
add_filter( 'rwmb_frontend_validate', function( $validate, $config ) {
if ( 'booking-fields' !== $config['id'] ) {
return $validate;
}
$disable_dates = dates_disable(get_the_ID());
$checkin = date("Y-m-d", strtotime($_POST['group_booking_check_in']));
$checkout = date("Y-m-d", strtotime($_POST['group_booking_check_out']));
if ( false !== array_search($checkin, $disable_dates)) {
$validate = false;
} else {
update_post_meta($config['post_id'], 'group_booking_room', get_the_ID());
}
return $validate;
}, 10, 2 );
view raw
function.php
hosted with ❤ by GitHub

 

Get all tools to build your hotel booking

Leave a Reply

Your email address will not be published. Required fields are marked *