Xây dựng website đặt phòng khách sạn với Meta Box (P3) – Cho phép khách hàng đặt phòng

Mình lại quay trở lại với phần tiếp theo của series Tạo và quản lý đặt phòng khách sạn với Meta Box rồi đây. Tiếp theo đây, chúng ta sẽ tạo phần đặt phòng ngay trên trang thông tin chi tiết từng phòng khách sạn để khách hàng của bạn có thể tự tạo booking.

Phần đặt phòng này có hơi khác so với phần đặt phòng đã tạo ở phần 2, chủ yếu liên quan đến chức năng của từng field. Hãy cùng xem cách làm như nào nhé.

Chuẩn bị

Ở phần này, chúng ta sẽ cho phép người dùng tự chọn ngày đặt phòng trên lịch một cách trực quan và không cho phép họ chọn những ngày đã hết phòng hay ngày trong quá khứ. Vì vậy, ngoài các plugin mà mình đã liệt kê ở phần 1, chúng ta sẽ cần dùng Datepicker, một thư viện Javascript, để giúp làm việc này.

Mình chọn dùng thư viện Jquery UI Datepicker. Đây cũng chính là thư viện mà Meta Box plugin dùng cho loại field dạng Date nên mình không cần phải import gì thêm, khá là tiện. Bạn chỉ cần đọc thêm chút tài liệu để hiểu thêm về các callback cũng như các tham số để tiện cho việc sử dụng thôi nhé.

Nào, bây giờ cùng bắt tay vào hoàn thiện nốt trang đặt phòng của bạn nhé.

Hiển thị form booking lên trang chi tiết phòng khách sạn cho khách hàng sử dụng

phần 2, mình đã hướng dẫn bạn tạo một trang đặt phòng ở frontend, nhưng là cho nhân viên của bạn sử dụng nội bộ. Thì ở đây, mình sẽ lấy đúng mẫu đặt phòng với các trường thông tin như ở trang đó để cho khách hàng của bạn sử dụng.

Theo logic thì khi khách hàng vào xem thông tin phòng, họ có thể sẽ tiến hành đặt phòng ngay tại trang này. Vì vậy, mình sẽ cho hiển thị luôn form đặt phòng ngay trên trang giới thiệu chi tiết về phòng khách sạn đã tạo ở phần 1.

Ngoài ra, vì là form cho khách hàng điền nên sẽ có một số trường không cần thiết hoặc cần điều chỉnh chức năng. Mình sẽ thực hiện ở bước sau để ẩn hoặc thay đổi chúng. Ở bước này, mình tạm cho hiển thị tất cả các field lên đã.

Bước 1: Lấy shortcode của nhóm field cho form booking

Trong admin dashboard, bạn đi đến menu Meta Box > Custom Fields để xem danh sách toàn bộ các nhóm field (field group) mà bạn đang có. Ở bên cạnh từng nhóm field sẽ có một đoạn shortcode như ảnh dưới đây.

Shortcode của nhóm field

Bạn copy shortcode của nhóm field mà mình đã tạo trước đó cho form booking nhé.

Bước 2: Đặt shortcode lên trang chi tiết phòng khách sạn

Nếu bạn sử dụng một Page Builder plugin thì việc đưa form đặt phòng này lên trang sẽ rất đơn giản. Ví dụ như với Elementor, bạn chỉ cần thêm một widget dạng shortcode vào vị trí bạn muốn, sau đó dán đoạn code sau vào phần nội dung của widget là được:

Một widget dạng shortcode

Nhưng nếu bạn không sử dụng page builder plugin, thì bạn sẽ cần dùng đến code một chút:

  1. Tạo một file có tên là single-room.php trong thư mục theme. Trong đó “room” chính là slug của post type sử dụng cho trang chi tiết phòng.
  2. Copy đoạn code có trong file single.php sang file single-room.php.
  3. Dùng hàm do_shortcode() của WordPress như dưới đây và đưa nó vào vị trí bạn muốn hiển thị.
<?php echo do_shortcode(‘[mb_frontend_form id='booking-fields' post_fields='title,content']’); ?>

Trong đó, [mb_frontend_form id='booking-fields' post_fields='title,content'] là đoạn shortcode mà mình đã copy ở bước 1.

Hàm do_shortcode
Đặt hàm do_shortcode() vào vị trí bạn muốn

Kết quả, bạn sẽ thấy các field nằm trong form booking mà bạn đã tạo ở phần 2 sẽ được hiển thị lên trang chi tiết đặt phòng như sau:

Trang chi tiết đặt phòng

Lúc này, người dùng hoàn toàn có thể tự điền thông tin vào các field rồi nhé.

Ẩn các trường thông tin không cần thiết bằng CSS

Khách hàng của bạn không cần phải quan tâm đến một số trường có trong form booking như Order number, Booking date, Person in charge, … Nên mình sẽ cho ẩn chúng đi bằng cách sử dụng CSS.

Ở trong phần chỉnh sửa thông tin của từng nhóm custom field (mục Meta Box > Custom Fields), bạn tìm đến trường thông tin bạn cần ẩn nhé. Ví dụ ở đây là trường Order Number. Sau đó bạn chuyển tới tab Appearance và nhập “hide-frontend” vào ô Custom CSS Class.

CSS Class cho custom fields
Thêm CSS Class cho mỗi custom fields

Bạn làm tương tự với các field khác với cùng 1 CSS Class là “hide-frontend” nhé.

Cuối cùng, bạn tìm đến file style.css trong thư mục theme hoặc là vào giao diện Customizer của theme (Appearance > Customize) và đưa đoạn code css sau vào:

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

Ngoài ra, mình cũng muốn ẩn nút Add Room luôn, nên mình cũng thêm cả đoạn code CSS sau vào theme:

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

Ngay sau đó, bạn sẽ thấy nút Add Room và các field không cần thiết sẽ được ẩn đi.

Xử lý chức năng cho 2 trường Check-in và Check-out

Trước khi tiến hành xử lý chức năng cho 2 trường này, mình cần tạo một file custom.js để xử lý JS cho riêng các trang chi tiết phòng khách sạn. Việc này sẽ giúp tối ưu tốc độ và tránh những lỗi không đáng có vì không cần truy cập vào các trang khác.

Tạo file custom.js trong thư mục theme của bạn, và dán đoạn code sau vào file functions.php:

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’);

Trong đó:

  • is_singular(‘room’): là để kiểm tra xem trang hiện tại đang truy cập có phải là trang chi tiết phòng khách sạn hay không (có slug của post type là room). Cái này là để quy định rằng file custom.js là chỉ dùng cho trang chi tiết phòng khách sạn thôi.
  • Hàm wp_enqueue_script là để import file custom.js
  • Hàm get_template_directory_url() là để trả về đường dẫn của theme

Tạo Date Range – Phạm vi ngày checkin và ngày checkout

Ngày checkin luôn phải trước ngày checkout, nên chúng ta cần đặt giới hạn cho ngày. Ví dụ khi người dùng đã chọn ngày checkin, thì tất cả các ngày trước ngày checkin sẽ được khoá lại và không thể chọn làm ngày checkout được.

Ở đây, mình sử dụng Jquery UI Datepicker với đoạn code sau và đưa vào filecustom.js.

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;
    }
} );

Trong đó, #group_booking_check_in#group_booking_check_out lần lượt là ID của 2 trường check-in và check-out của mình. Bạn cần đổi các ID này cho khớp với ID của các field của bạn nhé.

ID của fields
Lấy ID của fields

Kết quả sẽ như sau:

Lịch hiển thị

Kiểm tra ngày còn trống và khoá những ngày hết phòng

Đây là thao tác mấu chốt của cả series này. Mình đã tìm hiểu và thử khá nhiều cách, cuối cùng cũng tìm ra được một cách tối ưu và đơn giản như sau.

Bước 1: Tạo option lưu trữ lịch booking của các phòng

Mỗi khi có đặt phòng mới (1 booking order mới) thì giá trị của ngày đặt phòng sẽ được đẩy vào option này cùng với ID của phòng tương ứng để lưu trữ.

Giá trị của option sẽ là một mảng kiểu như sau:

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),
        )
    )
)

Trong đó:

  • room_1, room_2, … là ID của các phòng
  • booking_1, booking_2, … là ID của các booking
  • key_1, key_2, … là số thứ tự clone của group
  • date1, date2, … là ngày đã được đặt cho phòng đó tương ứng theo booking

Mỗi khi có đặt phòng, mình sẽ thêm giá trị ngày đặt “date” vào phòng tương ứng.

Mình thêm đoạn code sau vào file functions.php để tạo option có tên là rwmb_bookings với giá trị ban đầu khởi tạo là 1 mảng rỗng.

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

Bạn có thể tham khảo thêm cách tạo option mới trong WordPress tại đây.

Bước 2: Đưa giá trị vào option mỗi khi có booking mới

Mình đưa đoạn code sau vào file functions.php .

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 ); // Lấy giá trị của field group_booking đưa vào biến $booking
    if (empty($bookings) && !is_array($bookings)) return;
    $option = get_option('rwmb_bookings');
        foreach ($bookings as $key => $booking) { // Chạy vòng lặp biến $booking vì đây là 1 group field
            $room = $booking['room']; // $room là biến chứa id của phòng
            $begin = new DateTime( $booking['check_in'] ); // $begin là ngày bắt đầu cũng như ngày check in phòng
            $end = new DateTime( $booking['check_out'] ); // $end là ngày kết thúc cũng như ngày check out

            $interval = new DateInterval('P1D');
            $daterange = new DatePeriod($begin, $interval ,$end); // Hàm DatePeriod trả về mảng chứa tất cả ngày giữa 2 ngày check in và check out đưa vào
            $dates_booking = array();
            foreach($daterange as $date){
                array_push($dates_booking, $date->format("Y-m-d")); // Đưa giá trị vào biến $dates_booking
            }
            $option[$room][$post_id][$key] = $dates_booking; // Gán lại giá trị cho biến $option
        }
        update_option('rwmb_bookings', $option); // Cập nhật option 'rwmb_bookings'
}

Ở đây, mình dùng hook  rwmb_{$field_id}_after_save_post của Meta Box để đẩy giá trị vào biến option.  $field_id.của mình là booking-fields. Như vậy, mỗi khi lưu các bài viết có chứa field group có ID là booking-fields (tức là có một booking mới), hook này sẽ thực hiện các hành động sau:

  1. Lấy giá trị của ngày check in và ngày check out
  2. Dùng hàm DatePeriod của PHP để đưa ra mảng chứa tất cả các ngày ở giữa 2 ngày đó
  3. Đẩy cả chuỗi ngày trên (không bao gồm ngày checkout) vào biến option với room_id tương ứng.

Bước 3: Kiểm tra ngày hết phòng và còn phòng từ biến option trên

Thêm đoạn code sau vào file functions.php.

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);
}

Bằng đoạn code trên, chúng ta đã lấy room_id của phòng khách sạn mà khách hàng đang xem và so sánh nó với biến option ở trên để xuất ra một mảng chỉ chứa các ngày có đặt phòng và số lượng phòng đã được đặt tương ứng như sau:

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
)

Tức là phòng đang xem có 6 ngày có đặt phòng, trong đó ngày 2019-12-19 có 2 phòng đã được đặt, những ngày khác đều chỉ có 1 phòng được đặt. Nhưng như vậy mới chỉ biết là ngày nào có đặt phòng, chứ chưa thực sự biết được ngày nào hết phòng nếu 1 loại phòng của bạn có nhiều hơn 1 phòng.

Ví dụ, phòng đang xem là phòng Family chẳng hạn, và có 2 phòng loại này. Như vậy có nghĩa là chỉ có ngày 2019-12-19 đã hết phòng Family và cần bị khoá để không ai có thể chọn trong form booking. Còn những ngày khác đều có thể đặt tiếp được.

Room_id kiểm tra số lượng phòng

Vì vậy, ta tiếp tục dùng room_id của phòng đang xem để kiểm tra số lượng phòng của loại phòng đó để xem ngày nào hết phòng, ngày nào không.

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

Cuối cùng, bạn đổi tên hàm ở trên từ amounts_by_date($room_id) sang dates_disable() như sau:

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;
}

Hàm này sẽ chỉ trả về ngày cần khoá. Do đó, khi bạn muốn dùng đến ngày cần khoá, thì bạn chỉ cần gọi biến:

$disable = dates_disable($room_id);

Lưu ý: Thay vì đưa vào cùng 1 hàm như mình làm, thì bạn cũng có thể dùng 1 biến gán bằng hàm trên, rồi tạo hàm mới để kiểm tra nhé.

Bước 4: Khóa những ngày đã hết phòng cho trường check in và check out bằng Javascript

Ta sẽ dùng 1 biến JS để gán cho 1 biến PHP để ẩn ngày (dùng biến dates_disable) từ jQuery Datepicker UI.

Chèn đoạn code dưới đây vào hàm import file custom.js, vào phía dưới đoạn wp_enqueue_script()

wp_localize_script( 'custom-script', 'disable_dates', json_encode(dates_disable(get_the_ID())));
Truyền biến PHP “dates_disable" vào JS
Truyền biến PHP dates_disable vào JS

Để kiểm tra xem bạn đã làm đúng hay chưa, bạn có thể console.log(disable_dates) rồi mở tab console trên trang chi tiết phòng khách sạn để xem. Nhưng nhớ tạo sẵn vài booking trước đó thì mới kiểm tra được nhé.

Tiếp theo, bạn thêm đoạn code sau vào file custom.js để khoá các ngày hết phòng cho trường checkin và checkout.

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

Trong đó:

  • from, to là 2 biến đã được sử dụng ở phần tạo Date Range.
  • Dates_disable là mảng chứa các ngày cần ẩn đi mà ta đã khai báo ở trên

Bạn sẽ thấy ngày 2019-12-19 đã được khoá trên lịch như sau:

Khóa ngày trên lịch

Bước 5: Tối ưu lại giá trị biến option

Như bạn thấy đấy, biến option này sẽ lưu vô hạn các giá trị của các ngày đặt phòng. Điều này sẽ làm nặng cơ sở dữ liệu là làm chậm quá trình chạy hàm kiểm tra phòng trống.

Thế nên, bạn sẽ cần tối ưu nó thêm một chút để loại bỏ đi những booking có ngày trước ngày hiện tại, giúp giảm thiểu số lượng các giá trị có trong biến option.

Chúng ta sẽ sử dụng cả 2 hàm dưới đây:

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' );

Trong đó:

  • Hàm optimal_bookings_option() giúp kiểm tra các ngày đặt phòng so với ngày hiện tại và loại những ngày trong quá khứ ra khỏi biến Option.
  • Hàm array_filter_recursive() giúp lọc và loại bỏ các giá trị có mảng rỗng hoặc giá trị rỗng.

Lý do mình sử dụng cả array_filter_recursive() đó là vì sau khi mình xoá các ngày booking thì thấy xuất hiện các biến chứa giá trị rỗng trong mảng. Ví dụ:

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"
        }
    }
}

Mảng [451] chứa 2 mảng rỗng mà không có giá trị gì cả. Hàm array_filter_recursive() chính là để loại bỏ những mảng kiểu như vậy đi. Mình đã tìm hiểu và áp dụng hàm này từ một tác giả trên php.net, bạn có thể tham khảo.

Đặt thêm điều kiện chọn ngày đặt phòng

Tuy là chúng ta đã khoá được những ngày hết phòng, nhưng đấy mới chỉ là xử lý để hiển thị ra như vậy thôi. Nếu khách hàng của bạn là coder, thì họ vẫn có thể hack được để chọn ngày đã bị khoá, hoặc chọn ngày đặt phòng là một ngày trong quá khứ.

Vì vậy, chúng ta vẫn nên đặt thêm điều kiện để hạn chế những trường hợp trên:

  • Ngày check-in và check-out không được trùng với những ngày đã bị khoá
  • Ngày check-in phải lớn hơn hoặc bằng ngày hiện tại

Nếu 1 trong 2, hoặc cả hai điều kiện trên không được đáp ứng, thì tức là booking đó không hợp lệ.

Mình sẽ dùng filter rwmb_frontend_validate để kiểm tra các điều kiện trên.

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 );

Qua đó, mình kiểm tra xem đặt phòng có hợp lệ không, nếu không sẽ thông báo cho người dùng là đặt phòng không thành công.

Kết luận

Đến đây, bạn đã hoàn thành việc cho phép người dùng đặt phòng từ trang chi tiết phòng khách sạn rồi đó. Vậy là toàn bộ các phần liên quan đến đặt phòng ở backend, frontend, cho khách hàng, hay nhân viên nội bộ sử dụng cũng đã đều hoàn thiện.

Tiếp theo và cũng là phần cuối cùng để tạo trang quản lý các booking, giúp chủ khách sạn & nhân viên của họ có thể nhìn tổng quan xem loại phòng nào còn trống những ngày nào trên lịch. Mình sẽ tiếp tục hướng dẫn này vào bài tiếp theo. Các bạn hãy đón đọc nhé.

Ngoài ra, để các bạn tiện theo dõi, đây là toàn bộ source code mình đã sử dụng, các bạn tham khảo nhé:

Toàn bộ source code của file custom.js:

https://gist.github.com/rilwis/b7836dfac1300ade67b3da9555ef5bab

Toàn bộ source code của file single-room.php:

https://gist.github.com/rilwis/8b30ae28789ff8a8a30273299835c28c

Toàn bộ source code của file functions.php:

https://gist.github.com/rilwis/9477615662022704307bd668eec10c81

Để lại bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *