1138 lines
37 KiB
PHP
1138 lines
37 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* Booking post type.
|
||
|
|
*
|
||
|
|
* Custom post type for BnB bookings.
|
||
|
|
*
|
||
|
|
* @package Magdev\WpBnb\PostTypes
|
||
|
|
*/
|
||
|
|
|
||
|
|
declare( strict_types=1 );
|
||
|
|
|
||
|
|
namespace Magdev\WpBnb\PostTypes;
|
||
|
|
|
||
|
|
use Magdev\WpBnb\Pricing\Calculator;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Booking post type class.
|
||
|
|
*/
|
||
|
|
final class Booking {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Post type slug.
|
||
|
|
*
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public const POST_TYPE = 'bnb_booking';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Meta key prefix.
|
||
|
|
*
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
private const META_PREFIX = '_bnb_booking_';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the post type.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function init(): void {
|
||
|
|
add_action( 'init', array( self::class, 'register' ) );
|
||
|
|
add_action( 'add_meta_boxes', array( self::class, 'add_meta_boxes' ) );
|
||
|
|
add_action( 'save_post_' . self::POST_TYPE, array( self::class, 'save_meta' ), 10, 2 );
|
||
|
|
add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( self::class, 'add_columns' ) );
|
||
|
|
add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( self::class, 'render_column' ), 10, 2 );
|
||
|
|
add_filter( 'manage_edit-' . self::POST_TYPE . '_sortable_columns', array( self::class, 'sortable_columns' ) );
|
||
|
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||
|
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||
|
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||
|
|
add_filter( 'wp_insert_post_data', array( self::class, 'auto_generate_title' ), 10, 2 );
|
||
|
|
add_action( 'admin_notices', array( self::class, 'show_conflict_notice' ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register the post type.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function register(): void {
|
||
|
|
$labels = array(
|
||
|
|
'name' => _x( 'Bookings', 'post type general name', 'wp-bnb' ),
|
||
|
|
'singular_name' => _x( 'Booking', 'post type singular name', 'wp-bnb' ),
|
||
|
|
'menu_name' => _x( 'Bookings', 'admin menu', 'wp-bnb' ),
|
||
|
|
'name_admin_bar' => _x( 'Booking', 'add new on admin bar', 'wp-bnb' ),
|
||
|
|
'add_new' => _x( 'Add New', 'booking', 'wp-bnb' ),
|
||
|
|
'add_new_item' => __( 'Add New Booking', 'wp-bnb' ),
|
||
|
|
'new_item' => __( 'New Booking', 'wp-bnb' ),
|
||
|
|
'edit_item' => __( 'Edit Booking', 'wp-bnb' ),
|
||
|
|
'view_item' => __( 'View Booking', 'wp-bnb' ),
|
||
|
|
'all_items' => __( 'Bookings', 'wp-bnb' ),
|
||
|
|
'search_items' => __( 'Search Bookings', 'wp-bnb' ),
|
||
|
|
'parent_item_colon' => __( 'Parent Bookings:', 'wp-bnb' ),
|
||
|
|
'not_found' => __( 'No bookings found.', 'wp-bnb' ),
|
||
|
|
'not_found_in_trash' => __( 'No bookings found in Trash.', 'wp-bnb' ),
|
||
|
|
'archives' => __( 'Booking archives', 'wp-bnb' ),
|
||
|
|
'insert_into_item' => __( 'Insert into booking', 'wp-bnb' ),
|
||
|
|
'uploaded_to_this_item' => __( 'Uploaded to this booking', 'wp-bnb' ),
|
||
|
|
'filter_items_list' => __( 'Filter bookings list', 'wp-bnb' ),
|
||
|
|
'items_list_navigation' => __( 'Bookings list navigation', 'wp-bnb' ),
|
||
|
|
'items_list' => __( 'Bookings list', 'wp-bnb' ),
|
||
|
|
);
|
||
|
|
|
||
|
|
$args = array(
|
||
|
|
'labels' => $labels,
|
||
|
|
'public' => false,
|
||
|
|
'publicly_queryable' => false,
|
||
|
|
'show_ui' => true,
|
||
|
|
'show_in_menu' => 'wp-bnb',
|
||
|
|
'query_var' => false,
|
||
|
|
'capability_type' => 'post',
|
||
|
|
'has_archive' => false,
|
||
|
|
'hierarchical' => false,
|
||
|
|
'menu_position' => null,
|
||
|
|
'menu_icon' => 'dashicons-calendar-alt',
|
||
|
|
'supports' => array( 'revisions' ),
|
||
|
|
'show_in_rest' => true,
|
||
|
|
'rest_base' => 'bookings',
|
||
|
|
'rest_controller_class' => 'WP_REST_Posts_Controller',
|
||
|
|
);
|
||
|
|
|
||
|
|
register_post_type( self::POST_TYPE, $args );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add meta boxes.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function add_meta_boxes(): void {
|
||
|
|
add_meta_box(
|
||
|
|
'bnb_booking_room_dates',
|
||
|
|
__( 'Room & Dates', 'wp-bnb' ),
|
||
|
|
array( self::class, 'render_room_dates_meta_box' ),
|
||
|
|
self::POST_TYPE,
|
||
|
|
'normal',
|
||
|
|
'high'
|
||
|
|
);
|
||
|
|
|
||
|
|
add_meta_box(
|
||
|
|
'bnb_booking_guest',
|
||
|
|
__( 'Guest Information', 'wp-bnb' ),
|
||
|
|
array( self::class, 'render_guest_meta_box' ),
|
||
|
|
self::POST_TYPE,
|
||
|
|
'normal',
|
||
|
|
'high'
|
||
|
|
);
|
||
|
|
|
||
|
|
add_meta_box(
|
||
|
|
'bnb_booking_pricing',
|
||
|
|
__( 'Pricing', 'wp-bnb' ),
|
||
|
|
array( self::class, 'render_pricing_meta_box' ),
|
||
|
|
self::POST_TYPE,
|
||
|
|
'normal',
|
||
|
|
'default'
|
||
|
|
);
|
||
|
|
|
||
|
|
add_meta_box(
|
||
|
|
'bnb_booking_status',
|
||
|
|
__( 'Status & Notes', 'wp-bnb' ),
|
||
|
|
array( self::class, 'render_status_meta_box' ),
|
||
|
|
self::POST_TYPE,
|
||
|
|
'side',
|
||
|
|
'high'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Render room and dates meta box.
|
||
|
|
*
|
||
|
|
* @param \WP_Post $post Current post object.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function render_room_dates_meta_box( \WP_Post $post ): void {
|
||
|
|
wp_nonce_field( 'bnb_booking_meta', 'bnb_booking_meta_nonce' );
|
||
|
|
|
||
|
|
$room_id = get_post_meta( $post->ID, self::META_PREFIX . 'room_id', true );
|
||
|
|
$check_in = get_post_meta( $post->ID, self::META_PREFIX . 'check_in', true );
|
||
|
|
$check_out = get_post_meta( $post->ID, self::META_PREFIX . 'check_out', true );
|
||
|
|
|
||
|
|
$rooms = get_posts(
|
||
|
|
array(
|
||
|
|
'post_type' => Room::POST_TYPE,
|
||
|
|
'post_status' => 'publish',
|
||
|
|
'posts_per_page' => -1,
|
||
|
|
'orderby' => 'title',
|
||
|
|
'order' => 'ASC',
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
// Group rooms by building.
|
||
|
|
$rooms_by_building = array();
|
||
|
|
foreach ( $rooms as $room ) {
|
||
|
|
$building = Room::get_building( $room->ID );
|
||
|
|
$building_name = $building ? $building->post_title : __( 'No Building', 'wp-bnb' );
|
||
|
|
$building_id = $building ? $building->ID : 0;
|
||
|
|
if ( ! isset( $rooms_by_building[ $building_id ] ) ) {
|
||
|
|
$rooms_by_building[ $building_id ] = array(
|
||
|
|
'name' => $building_name,
|
||
|
|
'rooms' => array(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
$rooms_by_building[ $building_id ]['rooms'][] = $room;
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
<table class="form-table">
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<label for="bnb_booking_room_id"><?php esc_html_e( 'Room', 'wp-bnb' ); ?> <span class="required">*</span></label>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<select id="bnb_booking_room_id" name="bnb_booking_room_id" class="widefat" required>
|
||
|
|
<option value=""><?php esc_html_e( '— Select Room —', 'wp-bnb' ); ?></option>
|
||
|
|
<?php foreach ( $rooms_by_building as $building_data ) : ?>
|
||
|
|
<optgroup label="<?php echo esc_attr( $building_data['name'] ); ?>">
|
||
|
|
<?php foreach ( $building_data['rooms'] as $room ) : ?>
|
||
|
|
<?php
|
||
|
|
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
||
|
|
$room_label = $room->post_title;
|
||
|
|
if ( $room_number ) {
|
||
|
|
$room_label .= ' (#' . $room_number . ')';
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
<option value="<?php echo esc_attr( $room->ID ); ?>" <?php selected( $room_id, $room->ID ); ?>>
|
||
|
|
<?php echo esc_html( $room_label ); ?>
|
||
|
|
</option>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</optgroup>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</select>
|
||
|
|
<?php if ( empty( $rooms ) ) : ?>
|
||
|
|
<p class="description">
|
||
|
|
<?php
|
||
|
|
printf(
|
||
|
|
/* translators: %s: Link to add new room */
|
||
|
|
esc_html__( 'No rooms found. %s first.', 'wp-bnb' ),
|
||
|
|
'<a href="' . esc_url( admin_url( 'post-new.php?post_type=' . Room::POST_TYPE ) ) . '">' . esc_html__( 'Add a room', 'wp-bnb' ) . '</a>'
|
||
|
|
);
|
||
|
|
?>
|
||
|
|
</p>
|
||
|
|
<?php endif; ?>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<label for="bnb_booking_check_in"><?php esc_html_e( 'Check-in Date', 'wp-bnb' ); ?> <span class="required">*</span></label>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<input type="date" id="bnb_booking_check_in" name="bnb_booking_check_in"
|
||
|
|
value="<?php echo esc_attr( $check_in ); ?>" class="regular-text" required>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<label for="bnb_booking_check_out"><?php esc_html_e( 'Check-out Date', 'wp-bnb' ); ?> <span class="required">*</span></label>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<input type="date" id="bnb_booking_check_out" name="bnb_booking_check_out"
|
||
|
|
value="<?php echo esc_attr( $check_out ); ?>" class="regular-text" required>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<?php esc_html_e( 'Duration', 'wp-bnb' ); ?>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<div id="bnb-booking-nights-display" class="bnb-booking-info">
|
||
|
|
<?php
|
||
|
|
if ( $check_in && $check_out ) {
|
||
|
|
$nights = self::calculate_nights( $check_in, $check_out );
|
||
|
|
printf(
|
||
|
|
/* translators: %d: Number of nights */
|
||
|
|
esc_html( _n( '%d night', '%d nights', $nights, 'wp-bnb' ) ),
|
||
|
|
$nights
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
esc_html_e( 'Select dates to see duration', 'wp-bnb' );
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<?php esc_html_e( 'Availability', 'wp-bnb' ); ?>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<div id="bnb-booking-availability-display" class="bnb-booking-info">
|
||
|
|
<?php esc_html_e( 'Select room and dates to check availability', 'wp-bnb' ); ?>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Render guest information meta box.
|
||
|
|
*
|
||
|
|
* @param \WP_Post $post Current post object.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function render_guest_meta_box( \WP_Post $post ): void {
|
||
|
|
$guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true );
|
||
|
|
$guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true );
|
||
|
|
$guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true );
|
||
|
|
$adults = get_post_meta( $post->ID, self::META_PREFIX . 'adults', true );
|
||
|
|
$children = get_post_meta( $post->ID, self::META_PREFIX . 'children', true );
|
||
|
|
$guest_notes = get_post_meta( $post->ID, self::META_PREFIX . 'guest_notes', true );
|
||
|
|
?>
|
||
|
|
<table class="form-table">
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<label for="bnb_booking_guest_name"><?php esc_html_e( 'Guest Name', 'wp-bnb' ); ?> <span class="required">*</span></label>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<input type="text" id="bnb_booking_guest_name" name="bnb_booking_guest_name"
|
||
|
|
value="<?php echo esc_attr( $guest_name ); ?>" class="regular-text" required>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<label for="bnb_booking_guest_email"><?php esc_html_e( 'Email', 'wp-bnb' ); ?></label>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<input type="email" id="bnb_booking_guest_email" name="bnb_booking_guest_email"
|
||
|
|
value="<?php echo esc_attr( $guest_email ); ?>" class="regular-text">
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<label for="bnb_booking_guest_phone"><?php esc_html_e( 'Phone', 'wp-bnb' ); ?></label>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<input type="tel" id="bnb_booking_guest_phone" name="bnb_booking_guest_phone"
|
||
|
|
value="<?php echo esc_attr( $guest_phone ); ?>" class="regular-text">
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<?php esc_html_e( 'Guests', 'wp-bnb' ); ?>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<label for="bnb_booking_adults"><?php esc_html_e( 'Adults', 'wp-bnb' ); ?></label>
|
||
|
|
<input type="number" id="bnb_booking_adults" name="bnb_booking_adults"
|
||
|
|
value="<?php echo esc_attr( $adults ?: '1' ); ?>" class="small-text" min="1" max="20">
|
||
|
|
|
||
|
|
<label for="bnb_booking_children"><?php esc_html_e( 'Children', 'wp-bnb' ); ?></label>
|
||
|
|
<input type="number" id="bnb_booking_children" name="bnb_booking_children"
|
||
|
|
value="<?php echo esc_attr( $children ?: '0' ); ?>" class="small-text" min="0" max="20">
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<label for="bnb_booking_guest_notes"><?php esc_html_e( 'Guest Notes', 'wp-bnb' ); ?></label>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<textarea id="bnb_booking_guest_notes" name="bnb_booking_guest_notes"
|
||
|
|
rows="3" class="large-text"><?php echo esc_textarea( $guest_notes ); ?></textarea>
|
||
|
|
<p class="description"><?php esc_html_e( 'Special requests or notes from the guest.', 'wp-bnb' ); ?></p>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Render pricing meta box.
|
||
|
|
*
|
||
|
|
* @param \WP_Post $post Current post object.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function render_pricing_meta_box( \WP_Post $post ): void {
|
||
|
|
$calculated_price = get_post_meta( $post->ID, self::META_PREFIX . 'calculated_price', true );
|
||
|
|
$price_breakdown = get_post_meta( $post->ID, self::META_PREFIX . 'price_breakdown', true );
|
||
|
|
$override_price = get_post_meta( $post->ID, self::META_PREFIX . 'override_price', true );
|
||
|
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||
|
|
?>
|
||
|
|
<table class="form-table">
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<?php esc_html_e( 'Calculated Price', 'wp-bnb' ); ?>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<div id="bnb-booking-price-display" class="bnb-booking-price">
|
||
|
|
<?php
|
||
|
|
if ( $calculated_price ) {
|
||
|
|
echo '<strong>' . esc_html( Calculator::formatPrice( (float) $calculated_price ) ) . '</strong>';
|
||
|
|
} else {
|
||
|
|
esc_html_e( 'Price will be calculated when room and dates are selected.', 'wp-bnb' );
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
</div>
|
||
|
|
<button type="button" id="bnb-recalculate-price" class="button button-secondary">
|
||
|
|
<?php esc_html_e( 'Recalculate Price', 'wp-bnb' ); ?>
|
||
|
|
</button>
|
||
|
|
<input type="hidden" id="bnb_booking_calculated_price" name="bnb_booking_calculated_price"
|
||
|
|
value="<?php echo esc_attr( $calculated_price ); ?>">
|
||
|
|
<input type="hidden" id="bnb_booking_price_breakdown" name="bnb_booking_price_breakdown"
|
||
|
|
value="<?php echo esc_attr( is_array( $price_breakdown ) ? wp_json_encode( $price_breakdown ) : '' ); ?>">
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<?php if ( $price_breakdown && is_array( $price_breakdown ) ) : ?>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<?php esc_html_e( 'Price Breakdown', 'wp-bnb' ); ?>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<div id="bnb-booking-breakdown-display" class="bnb-booking-breakdown">
|
||
|
|
<?php echo self::format_price_breakdown( $price_breakdown ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<?php endif; ?>
|
||
|
|
<tr>
|
||
|
|
<th scope="row">
|
||
|
|
<label for="bnb_booking_override_price"><?php esc_html_e( 'Override Price', 'wp-bnb' ); ?></label>
|
||
|
|
</th>
|
||
|
|
<td>
|
||
|
|
<input type="number" id="bnb_booking_override_price" name="bnb_booking_override_price"
|
||
|
|
value="<?php echo esc_attr( $override_price ); ?>" class="small-text" min="0" step="0.01">
|
||
|
|
<span><?php echo esc_html( $currency ); ?></span>
|
||
|
|
<p class="description"><?php esc_html_e( 'Leave empty to use calculated price. Enter a value to override.', 'wp-bnb' ); ?></p>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Render status and notes meta box.
|
||
|
|
*
|
||
|
|
* @param \WP_Post $post Current post object.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function render_status_meta_box( \WP_Post $post ): void {
|
||
|
|
$status = get_post_meta( $post->ID, self::META_PREFIX . 'status', true ) ?: 'pending';
|
||
|
|
$notes = get_post_meta( $post->ID, self::META_PREFIX . 'notes', true );
|
||
|
|
$confirmed_at = get_post_meta( $post->ID, self::META_PREFIX . 'confirmed_at', true );
|
||
|
|
$statuses = self::get_booking_statuses();
|
||
|
|
$colors = self::get_status_colors();
|
||
|
|
?>
|
||
|
|
<p>
|
||
|
|
<label for="bnb_booking_status"><strong><?php esc_html_e( 'Booking Status', 'wp-bnb' ); ?></strong></label>
|
||
|
|
</p>
|
||
|
|
<select id="bnb_booking_status" name="bnb_booking_status" class="widefat">
|
||
|
|
<?php foreach ( $statuses as $key => $label ) : ?>
|
||
|
|
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $status, $key ); ?>
|
||
|
|
data-color="<?php echo esc_attr( $colors[ $key ] ?? '#ccc' ); ?>">
|
||
|
|
<?php echo esc_html( $label ); ?>
|
||
|
|
</option>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</select>
|
||
|
|
<div id="bnb-status-preview" class="bnb-status-preview" style="margin-top: 10px;">
|
||
|
|
<span class="bnb-status-badge" style="background-color: <?php echo esc_attr( $colors[ $status ] ?? '#ccc' ); ?>">
|
||
|
|
<?php echo esc_html( $statuses[ $status ] ?? $status ); ?>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<?php if ( $confirmed_at ) : ?>
|
||
|
|
<p class="bnb-status-timestamp">
|
||
|
|
<?php
|
||
|
|
printf(
|
||
|
|
/* translators: %s: Date and time */
|
||
|
|
esc_html__( 'Confirmed: %s', 'wp-bnb' ),
|
||
|
|
esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $confirmed_at ) ) )
|
||
|
|
);
|
||
|
|
?>
|
||
|
|
</p>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<hr>
|
||
|
|
|
||
|
|
<p>
|
||
|
|
<label for="bnb_booking_notes"><strong><?php esc_html_e( 'Internal Notes', 'wp-bnb' ); ?></strong></label>
|
||
|
|
</p>
|
||
|
|
<textarea id="bnb_booking_notes" name="bnb_booking_notes" rows="4" class="widefat"><?php echo esc_textarea( $notes ); ?></textarea>
|
||
|
|
<p class="description"><?php esc_html_e( 'Notes for internal use only.', 'wp-bnb' ); ?></p>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Save post meta.
|
||
|
|
*
|
||
|
|
* @param int $post_id Post ID.
|
||
|
|
* @param \WP_Post $post Post object.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function save_meta( int $post_id, \WP_Post $post ): void {
|
||
|
|
// Verify nonce.
|
||
|
|
if ( ! isset( $_POST['bnb_booking_meta_nonce'] ) ||
|
||
|
|
! wp_verify_nonce( sanitize_key( $_POST['bnb_booking_meta_nonce'] ), 'bnb_booking_meta' ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check autosave.
|
||
|
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check permissions.
|
||
|
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get values for conflict check.
|
||
|
|
$room_id = isset( $_POST['bnb_booking_room_id'] ) ? absint( $_POST['bnb_booking_room_id'] ) : 0;
|
||
|
|
$check_in = isset( $_POST['bnb_booking_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_check_in'] ) ) : '';
|
||
|
|
$check_out = isset( $_POST['bnb_booking_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_check_out'] ) ) : '';
|
||
|
|
$status = isset( $_POST['bnb_booking_status'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_status'] ) ) : 'pending';
|
||
|
|
|
||
|
|
// Validate dates.
|
||
|
|
if ( $check_in && $check_out && strtotime( $check_out ) <= strtotime( $check_in ) ) {
|
||
|
|
add_filter(
|
||
|
|
'redirect_post_location',
|
||
|
|
function ( $location ) {
|
||
|
|
return add_query_arg( 'booking_error', 'invalid_dates', $location );
|
||
|
|
}
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for conflicts (only for non-cancelled bookings).
|
||
|
|
if ( $room_id && $check_in && $check_out && 'cancelled' !== $status ) {
|
||
|
|
if ( self::has_conflict( $room_id, $check_in, $check_out, $post_id ) ) {
|
||
|
|
add_filter(
|
||
|
|
'redirect_post_location',
|
||
|
|
function ( $location ) {
|
||
|
|
return add_query_arg( 'booking_conflict', '1', $location );
|
||
|
|
}
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get old status for comparison.
|
||
|
|
$old_status = get_post_meta( $post_id, self::META_PREFIX . 'status', true );
|
||
|
|
|
||
|
|
// Room ID.
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'room_id', $room_id );
|
||
|
|
|
||
|
|
// Dates (validate format Y-m-d).
|
||
|
|
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $check_in ) ) {
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'check_in', $check_in );
|
||
|
|
}
|
||
|
|
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $check_out ) ) {
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'check_out', $check_out );
|
||
|
|
}
|
||
|
|
|
||
|
|
// Status (validate against allowed statuses).
|
||
|
|
if ( array_key_exists( $status, self::get_booking_statuses() ) ) {
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'status', $status );
|
||
|
|
|
||
|
|
// Track confirmation timestamp.
|
||
|
|
if ( 'confirmed' === $status && 'confirmed' !== $old_status ) {
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'confirmed_at', current_time( 'mysql' ) );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Guest text fields.
|
||
|
|
$guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' );
|
||
|
|
foreach ( $guest_fields as $field ) {
|
||
|
|
$key = 'bnb_booking_' . $field;
|
||
|
|
if ( isset( $_POST[ $key ] ) ) {
|
||
|
|
$value = wp_unslash( $_POST[ $key ] );
|
||
|
|
if ( 'guest_email' === $field ) {
|
||
|
|
$value = sanitize_email( $value );
|
||
|
|
} elseif ( 'guest_notes' === $field ) {
|
||
|
|
$value = sanitize_textarea_field( $value );
|
||
|
|
} else {
|
||
|
|
$value = sanitize_text_field( $value );
|
||
|
|
}
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . $field, $value );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Guest counts.
|
||
|
|
if ( isset( $_POST['bnb_booking_adults'] ) ) {
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'adults', absint( $_POST['bnb_booking_adults'] ) );
|
||
|
|
}
|
||
|
|
if ( isset( $_POST['bnb_booking_children'] ) ) {
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'children', absint( $_POST['bnb_booking_children'] ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
// Notes.
|
||
|
|
if ( isset( $_POST['bnb_booking_notes'] ) ) {
|
||
|
|
update_post_meta(
|
||
|
|
$post_id,
|
||
|
|
self::META_PREFIX . 'notes',
|
||
|
|
sanitize_textarea_field( wp_unslash( $_POST['bnb_booking_notes'] ) )
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Pricing.
|
||
|
|
$override_price = isset( $_POST['bnb_booking_override_price'] ) && '' !== $_POST['bnb_booking_override_price']
|
||
|
|
? floatval( $_POST['bnb_booking_override_price'] )
|
||
|
|
: null;
|
||
|
|
|
||
|
|
if ( null !== $override_price ) {
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'override_price', $override_price );
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'calculated_price', $override_price );
|
||
|
|
} else {
|
||
|
|
delete_post_meta( $post_id, self::META_PREFIX . 'override_price' );
|
||
|
|
|
||
|
|
// Calculate price if room and dates are set.
|
||
|
|
if ( $room_id && $check_in && $check_out ) {
|
||
|
|
try {
|
||
|
|
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||
|
|
$price = $calculator->calculate();
|
||
|
|
$breakdown = $calculator->getBreakdown();
|
||
|
|
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'calculated_price', $price );
|
||
|
|
update_post_meta( $post_id, self::META_PREFIX . 'price_breakdown', $breakdown );
|
||
|
|
} catch ( \Exception $e ) {
|
||
|
|
// Keep existing price if calculation fails.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Trigger status change action.
|
||
|
|
if ( $old_status && $status !== $old_status ) {
|
||
|
|
/**
|
||
|
|
* Fires when a booking status changes.
|
||
|
|
*
|
||
|
|
* @param int $post_id Booking post ID.
|
||
|
|
* @param string $old_status Previous status.
|
||
|
|
* @param string $status New status.
|
||
|
|
*/
|
||
|
|
do_action( 'wp_bnb_booking_status_changed', $post_id, $old_status, $status );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add custom columns to the post list.
|
||
|
|
*
|
||
|
|
* @param array $columns Existing columns.
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
public static function add_columns( array $columns ): array {
|
||
|
|
$new_columns = array();
|
||
|
|
foreach ( $columns as $key => $value ) {
|
||
|
|
$new_columns[ $key ] = $value;
|
||
|
|
if ( 'title' === $key ) {
|
||
|
|
$new_columns['room'] = __( 'Room', 'wp-bnb' );
|
||
|
|
$new_columns['guest'] = __( 'Guest', 'wp-bnb' );
|
||
|
|
$new_columns['dates'] = __( 'Dates', 'wp-bnb' );
|
||
|
|
$new_columns['nights'] = __( 'Nights', 'wp-bnb' );
|
||
|
|
$new_columns['price'] = __( 'Price', 'wp-bnb' );
|
||
|
|
$new_columns['status'] = __( 'Status', 'wp-bnb' );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Remove date column, we have our own dates.
|
||
|
|
unset( $new_columns['date'] );
|
||
|
|
return $new_columns;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Render custom column content.
|
||
|
|
*
|
||
|
|
* @param string $column Column name.
|
||
|
|
* @param int $post_id Post ID.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function render_column( string $column, int $post_id ): void {
|
||
|
|
switch ( $column ) {
|
||
|
|
case 'room':
|
||
|
|
$room_id = get_post_meta( $post_id, self::META_PREFIX . 'room_id', true );
|
||
|
|
if ( $room_id ) {
|
||
|
|
$room = get_post( $room_id );
|
||
|
|
if ( $room ) {
|
||
|
|
$building = Room::get_building( $room_id );
|
||
|
|
printf(
|
||
|
|
'<a href="%s">%s</a>',
|
||
|
|
esc_url( get_edit_post_link( $room_id ) ),
|
||
|
|
esc_html( $room->post_title )
|
||
|
|
);
|
||
|
|
if ( $building ) {
|
||
|
|
echo '<br><small>' . esc_html( $building->post_title ) . '</small>';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
echo '<em>' . esc_html__( 'Room deleted', 'wp-bnb' ) . '</em>';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
echo '—';
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'guest':
|
||
|
|
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
|
||
|
|
$guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true );
|
||
|
|
if ( $guest_name ) {
|
||
|
|
echo esc_html( $guest_name );
|
||
|
|
if ( $guest_email ) {
|
||
|
|
echo '<br><small><a href="mailto:' . esc_attr( $guest_email ) . '">' . esc_html( $guest_email ) . '</a></small>';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
echo '—';
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'dates':
|
||
|
|
$check_in = get_post_meta( $post_id, self::META_PREFIX . 'check_in', true );
|
||
|
|
$check_out = get_post_meta( $post_id, self::META_PREFIX . 'check_out', true );
|
||
|
|
if ( $check_in && $check_out ) {
|
||
|
|
$format = get_option( 'date_format' );
|
||
|
|
echo esc_html( wp_date( $format, strtotime( $check_in ) ) );
|
||
|
|
echo '<br><small>' . esc_html__( 'to', 'wp-bnb' ) . ' ' . esc_html( wp_date( $format, strtotime( $check_out ) ) ) . '</small>';
|
||
|
|
} else {
|
||
|
|
echo '—';
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'nights':
|
||
|
|
$check_in = get_post_meta( $post_id, self::META_PREFIX . 'check_in', true );
|
||
|
|
$check_out = get_post_meta( $post_id, self::META_PREFIX . 'check_out', true );
|
||
|
|
if ( $check_in && $check_out ) {
|
||
|
|
$nights = self::calculate_nights( $check_in, $check_out );
|
||
|
|
echo esc_html( $nights );
|
||
|
|
} else {
|
||
|
|
echo '—';
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'price':
|
||
|
|
$price = get_post_meta( $post_id, self::META_PREFIX . 'calculated_price', true );
|
||
|
|
if ( $price ) {
|
||
|
|
echo esc_html( Calculator::formatPrice( (float) $price ) );
|
||
|
|
$override = get_post_meta( $post_id, self::META_PREFIX . 'override_price', true );
|
||
|
|
if ( $override ) {
|
||
|
|
echo ' <span class="bnb-price-override" title="' . esc_attr__( 'Price manually overridden', 'wp-bnb' ) . '">*</span>';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
echo '—';
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'status':
|
||
|
|
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'pending';
|
||
|
|
$statuses = self::get_booking_statuses();
|
||
|
|
$colors = self::get_status_colors();
|
||
|
|
?>
|
||
|
|
<span class="bnb-status-badge" style="background-color: <?php echo esc_attr( $colors[ $status ] ?? '#ccc' ); ?>">
|
||
|
|
<?php echo esc_html( $statuses[ $status ] ?? $status ); ?>
|
||
|
|
</span>
|
||
|
|
<?php
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add sortable columns.
|
||
|
|
*
|
||
|
|
* @param array $columns Existing sortable columns.
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
public static function sortable_columns( array $columns ): array {
|
||
|
|
$columns['dates'] = 'check_in';
|
||
|
|
$columns['guest'] = 'guest_name';
|
||
|
|
$columns['status'] = 'status';
|
||
|
|
return $columns;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add filter dropdowns to admin list.
|
||
|
|
*
|
||
|
|
* @param string $post_type Current post type.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function add_filters( string $post_type ): void {
|
||
|
|
if ( self::POST_TYPE !== $post_type ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Room filter.
|
||
|
|
$rooms = get_posts(
|
||
|
|
array(
|
||
|
|
'post_type' => Room::POST_TYPE,
|
||
|
|
'post_status' => 'publish',
|
||
|
|
'posts_per_page' => -1,
|
||
|
|
'orderby' => 'title',
|
||
|
|
'order' => 'ASC',
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( ! empty( $rooms ) ) {
|
||
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
|
||
|
|
$selected_room = isset( $_GET['room_id'] ) ? absint( $_GET['room_id'] ) : 0;
|
||
|
|
?>
|
||
|
|
<select name="room_id">
|
||
|
|
<option value=""><?php esc_html_e( 'All Rooms', 'wp-bnb' ); ?></option>
|
||
|
|
<?php foreach ( $rooms as $room ) : ?>
|
||
|
|
<option value="<?php echo esc_attr( $room->ID ); ?>" <?php selected( $selected_room, $room->ID ); ?>>
|
||
|
|
<?php echo esc_html( $room->post_title ); ?>
|
||
|
|
</option>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</select>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
// Status filter.
|
||
|
|
$statuses = self::get_booking_statuses();
|
||
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
|
||
|
|
$selected_status = isset( $_GET['booking_status'] ) ? sanitize_text_field( wp_unslash( $_GET['booking_status'] ) ) : '';
|
||
|
|
?>
|
||
|
|
<select name="booking_status">
|
||
|
|
<option value=""><?php esc_html_e( 'All Statuses', 'wp-bnb' ); ?></option>
|
||
|
|
<?php foreach ( $statuses as $key => $label ) : ?>
|
||
|
|
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $selected_status, $key ); ?>>
|
||
|
|
<?php echo esc_html( $label ); ?>
|
||
|
|
</option>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</select>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Filter bookings by room and status in admin list.
|
||
|
|
*
|
||
|
|
* @param \WP_Query $query Current query.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function filter_query( \WP_Query $query ): void {
|
||
|
|
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( self::POST_TYPE !== $query->get( 'post_type' ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$meta_query = array();
|
||
|
|
|
||
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||
|
|
if ( ! empty( $_GET['room_id'] ) ) {
|
||
|
|
$meta_query[] = array(
|
||
|
|
'key' => self::META_PREFIX . 'room_id',
|
||
|
|
'value' => absint( $_GET['room_id'] ),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||
|
|
if ( ! empty( $_GET['booking_status'] ) ) {
|
||
|
|
$meta_query[] = array(
|
||
|
|
'key' => self::META_PREFIX . 'status',
|
||
|
|
'value' => sanitize_text_field( wp_unslash( $_GET['booking_status'] ) ),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( ! empty( $meta_query ) ) {
|
||
|
|
$meta_query['relation'] = 'AND';
|
||
|
|
$query->set( 'meta_query', $meta_query );
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle sorting.
|
||
|
|
$orderby = $query->get( 'orderby' );
|
||
|
|
if ( 'check_in' === $orderby ) {
|
||
|
|
$query->set( 'meta_key', self::META_PREFIX . 'check_in' );
|
||
|
|
$query->set( 'orderby', 'meta_value' );
|
||
|
|
} elseif ( 'guest_name' === $orderby ) {
|
||
|
|
$query->set( 'meta_key', self::META_PREFIX . 'guest_name' );
|
||
|
|
$query->set( 'orderby', 'meta_value' );
|
||
|
|
} elseif ( 'status' === $orderby ) {
|
||
|
|
$query->set( 'meta_key', self::META_PREFIX . 'status' );
|
||
|
|
$query->set( 'orderby', 'meta_value' );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Change title placeholder.
|
||
|
|
*
|
||
|
|
* @param string $placeholder Default placeholder.
|
||
|
|
* @param \WP_Post $post Current post.
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string {
|
||
|
|
if ( self::POST_TYPE === $post->post_type ) {
|
||
|
|
return __( 'Booking reference (auto-generated)', 'wp-bnb' );
|
||
|
|
}
|
||
|
|
return $placeholder;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Auto-generate booking reference as title.
|
||
|
|
*
|
||
|
|
* @param array $data Post data.
|
||
|
|
* @param array $postarr Post array.
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
public static function auto_generate_title( array $data, array $postarr ): array {
|
||
|
|
if ( self::POST_TYPE !== $data['post_type'] ) {
|
||
|
|
return $data;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only generate if title is empty or matches auto-generated pattern.
|
||
|
|
if ( empty( $data['post_title'] ) || preg_match( '/^BNB-\d{4}-\d{5}$/', $data['post_title'] ) ) {
|
||
|
|
$data['post_title'] = self::generate_reference();
|
||
|
|
}
|
||
|
|
|
||
|
|
return $data;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Show conflict notice in admin.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public static function show_conflict_notice(): void {
|
||
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Notice display only.
|
||
|
|
if ( isset( $_GET['booking_conflict'] ) && '1' === $_GET['booking_conflict'] ) {
|
||
|
|
?>
|
||
|
|
<div class="notice notice-error is-dismissible">
|
||
|
|
<p>
|
||
|
|
<strong><?php esc_html_e( 'Booking Conflict!', 'wp-bnb' ); ?></strong>
|
||
|
|
<?php esc_html_e( 'The selected dates overlap with an existing booking for this room. Please choose different dates.', 'wp-bnb' ); ?>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Notice display only.
|
||
|
|
if ( isset( $_GET['booking_error'] ) && 'invalid_dates' === $_GET['booking_error'] ) {
|
||
|
|
?>
|
||
|
|
<div class="notice notice-error is-dismissible">
|
||
|
|
<p>
|
||
|
|
<strong><?php esc_html_e( 'Invalid Dates!', 'wp-bnb' ); ?></strong>
|
||
|
|
<?php esc_html_e( 'Check-out date must be after check-in date.', 'wp-bnb' ); ?>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get booking status options.
|
||
|
|
*
|
||
|
|
* @return array<string, string>
|
||
|
|
*/
|
||
|
|
public static function get_booking_statuses(): array {
|
||
|
|
return array(
|
||
|
|
'pending' => __( 'Pending', 'wp-bnb' ),
|
||
|
|
'confirmed' => __( 'Confirmed', 'wp-bnb' ),
|
||
|
|
'checked_in' => __( 'Checked In', 'wp-bnb' ),
|
||
|
|
'checked_out' => __( 'Checked Out', 'wp-bnb' ),
|
||
|
|
'cancelled' => __( 'Cancelled', 'wp-bnb' ),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get status color codes.
|
||
|
|
*
|
||
|
|
* @return array<string, string>
|
||
|
|
*/
|
||
|
|
public static function get_status_colors(): array {
|
||
|
|
return array(
|
||
|
|
'pending' => '#dba617',
|
||
|
|
'confirmed' => '#00a32a',
|
||
|
|
'checked_in' => '#72aee6',
|
||
|
|
'checked_out' => '#646970',
|
||
|
|
'cancelled' => '#d63638',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get valid status transitions.
|
||
|
|
*
|
||
|
|
* @return array<string, array<string>>
|
||
|
|
*/
|
||
|
|
public static function get_status_transitions(): array {
|
||
|
|
return array(
|
||
|
|
'pending' => array( 'confirmed', 'cancelled' ),
|
||
|
|
'confirmed' => array( 'checked_in', 'cancelled' ),
|
||
|
|
'checked_in' => array( 'checked_out' ),
|
||
|
|
'checked_out' => array(),
|
||
|
|
'cancelled' => array( 'pending' ),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a status transition is valid.
|
||
|
|
*
|
||
|
|
* @param string $from Current status.
|
||
|
|
* @param string $to New status.
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public static function can_transition_to( string $from, string $to ): bool {
|
||
|
|
if ( $from === $to ) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
$transitions = self::get_status_transitions();
|
||
|
|
return isset( $transitions[ $from ] ) && in_array( $to, $transitions[ $from ], true );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a booking reference number.
|
||
|
|
*
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public static function generate_reference(): string {
|
||
|
|
$year = gmdate( 'Y' );
|
||
|
|
$count = wp_count_posts( self::POST_TYPE );
|
||
|
|
$total = ( $count->publish ?? 0 ) + ( $count->draft ?? 0 ) + ( $count->pending ?? 0 ) + ( $count->trash ?? 0 ) + 1;
|
||
|
|
return sprintf( 'BNB-%s-%05d', $year, $total );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate number of nights between two dates.
|
||
|
|
*
|
||
|
|
* @param string $check_in Check-in date (Y-m-d).
|
||
|
|
* @param string $check_out Check-out date (Y-m-d).
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
public static function calculate_nights( string $check_in, string $check_out ): int {
|
||
|
|
$start = new \DateTimeImmutable( $check_in );
|
||
|
|
$end = new \DateTimeImmutable( $check_out );
|
||
|
|
return max( 1, (int) $start->diff( $end )->days );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if dates conflict with existing bookings.
|
||
|
|
*
|
||
|
|
* @param int $room_id Room post ID.
|
||
|
|
* @param string $check_in Check-in date (Y-m-d).
|
||
|
|
* @param string $check_out Check-out date (Y-m-d).
|
||
|
|
* @param int|null $exclude_id Booking ID to exclude (for editing).
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public static function has_conflict( int $room_id, string $check_in, string $check_out, ?int $exclude_id = null ): bool {
|
||
|
|
$args = array(
|
||
|
|
'post_type' => self::POST_TYPE,
|
||
|
|
'post_status' => 'publish',
|
||
|
|
'posts_per_page' => 1,
|
||
|
|
'fields' => 'ids',
|
||
|
|
'meta_query' => array(
|
||
|
|
'relation' => 'AND',
|
||
|
|
array(
|
||
|
|
'key' => self::META_PREFIX . 'room_id',
|
||
|
|
'value' => $room_id,
|
||
|
|
),
|
||
|
|
array(
|
||
|
|
'key' => self::META_PREFIX . 'status',
|
||
|
|
'value' => 'cancelled',
|
||
|
|
'compare' => '!=',
|
||
|
|
),
|
||
|
|
// Overlap detection: existing.check_in < new.check_out AND existing.check_out > new.check_in.
|
||
|
|
array(
|
||
|
|
'key' => self::META_PREFIX . 'check_in',
|
||
|
|
'value' => $check_out,
|
||
|
|
'compare' => '<',
|
||
|
|
'type' => 'DATE',
|
||
|
|
),
|
||
|
|
array(
|
||
|
|
'key' => self::META_PREFIX . 'check_out',
|
||
|
|
'value' => $check_in,
|
||
|
|
'compare' => '>',
|
||
|
|
'type' => 'DATE',
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( $exclude_id ) {
|
||
|
|
$args['post__not_in'] = array( $exclude_id );
|
||
|
|
}
|
||
|
|
|
||
|
|
$conflicts = get_posts( $args );
|
||
|
|
return ! empty( $conflicts );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get room for a booking.
|
||
|
|
*
|
||
|
|
* @param int $booking_id Booking post ID.
|
||
|
|
* @return \WP_Post|null
|
||
|
|
*/
|
||
|
|
public static function get_room( int $booking_id ): ?\WP_Post {
|
||
|
|
$room_id = get_post_meta( $booking_id, self::META_PREFIX . 'room_id', true );
|
||
|
|
if ( ! $room_id ) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return get_post( $room_id );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get building for a booking (through room).
|
||
|
|
*
|
||
|
|
* @param int $booking_id Booking post ID.
|
||
|
|
* @return \WP_Post|null
|
||
|
|
*/
|
||
|
|
public static function get_building( int $booking_id ): ?\WP_Post {
|
||
|
|
$room = self::get_room( $booking_id );
|
||
|
|
if ( ! $room ) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return Room::get_building( $room->ID );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all bookings for a room.
|
||
|
|
*
|
||
|
|
* @param int $room_id Room post ID.
|
||
|
|
* @param array $args Additional query args.
|
||
|
|
* @return array<\WP_Post>
|
||
|
|
*/
|
||
|
|
public static function get_bookings_for_room( int $room_id, array $args = array() ): array {
|
||
|
|
$defaults = array(
|
||
|
|
'post_type' => self::POST_TYPE,
|
||
|
|
'post_status' => 'publish',
|
||
|
|
'posts_per_page' => -1,
|
||
|
|
'meta_query' => array(
|
||
|
|
array(
|
||
|
|
'key' => self::META_PREFIX . 'room_id',
|
||
|
|
'value' => $room_id,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
'orderby' => 'meta_value',
|
||
|
|
'meta_key' => self::META_PREFIX . 'check_in',
|
||
|
|
'order' => 'ASC',
|
||
|
|
);
|
||
|
|
|
||
|
|
return get_posts( array_merge( $defaults, $args ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format price breakdown for display.
|
||
|
|
*
|
||
|
|
* @param array $breakdown Price breakdown array.
|
||
|
|
* @return string HTML output.
|
||
|
|
*/
|
||
|
|
private static function format_price_breakdown( array $breakdown ): string {
|
||
|
|
$output = '<ul class="bnb-breakdown-list">';
|
||
|
|
|
||
|
|
if ( isset( $breakdown['tier'] ) ) {
|
||
|
|
$output .= '<li><strong>' . esc_html__( 'Pricing Tier:', 'wp-bnb' ) . '</strong> ';
|
||
|
|
$output .= esc_html( ucfirst( str_replace( '_', ' ', $breakdown['tier'] ) ) ) . '</li>';
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( isset( $breakdown['nights'] ) && is_array( $breakdown['nights'] ) ) {
|
||
|
|
$output .= '<li><strong>' . esc_html__( 'Nights:', 'wp-bnb' ) . '</strong> ' . count( $breakdown['nights'] ) . '</li>';
|
||
|
|
$output .= '<li><strong>' . esc_html__( 'Nightly Rate:', 'wp-bnb' ) . '</strong> ';
|
||
|
|
$output .= esc_html( Calculator::formatPrice( (float) ( $breakdown['nightly_rate'] ?? 0 ) ) ) . '</li>';
|
||
|
|
} elseif ( isset( $breakdown['weeks'] ) ) {
|
||
|
|
$output .= '<li><strong>' . esc_html__( 'Weeks:', 'wp-bnb' ) . '</strong> ' . esc_html( $breakdown['weeks'] ) . '</li>';
|
||
|
|
$output .= '<li><strong>' . esc_html__( 'Weekly Rate:', 'wp-bnb' ) . '</strong> ';
|
||
|
|
$output .= esc_html( Calculator::formatPrice( (float) ( $breakdown['weekly_rate'] ?? 0 ) ) ) . '</li>';
|
||
|
|
} elseif ( isset( $breakdown['months'] ) ) {
|
||
|
|
$output .= '<li><strong>' . esc_html__( 'Months:', 'wp-bnb' ) . '</strong> ' . esc_html( $breakdown['months'] ) . '</li>';
|
||
|
|
$output .= '<li><strong>' . esc_html__( 'Monthly Rate:', 'wp-bnb' ) . '</strong> ';
|
||
|
|
$output .= esc_html( Calculator::formatPrice( (float) ( $breakdown['monthly_rate'] ?? 0 ) ) ) . '</li>';
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( isset( $breakdown['total'] ) ) {
|
||
|
|
$output .= '<li><strong>' . esc_html__( 'Total:', 'wp-bnb' ) . '</strong> ';
|
||
|
|
$output .= esc_html( Calculator::formatPrice( (float) $breakdown['total'] ) ) . '</li>';
|
||
|
|
}
|
||
|
|
|
||
|
|
$output .= '</ul>';
|
||
|
|
|
||
|
|
return $output;
|
||
|
|
}
|
||
|
|
}
|