All checks were successful
Create Release Package / build-release (push) Successful in 1m5s
- Booking Custom Post Type with full management features - Room and guest relationship tracking - Check-in/check-out date management with validation - Booking status workflow (pending, confirmed, checked_in, checked_out, cancelled) - Automatic price calculation using existing Calculator - Availability system with real-time conflict detection - AJAX endpoint for instant availability validation - Calendar admin page with monthly view and room/building filters - Color-coded booking status display with legend - Email notifications for new bookings, confirmations, and cancellations - HTML email templates with placeholder-based system - Auto-generated booking references (BNB-YYYY-NNNNN) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
360 lines
11 KiB
PHP
360 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Calendar admin page.
|
|
*
|
|
* Displays availability calendar for rooms and buildings.
|
|
*
|
|
* @package Magdev\WpBnb\Admin
|
|
*/
|
|
|
|
declare( strict_types=1 );
|
|
|
|
namespace Magdev\WpBnb\Admin;
|
|
|
|
use Magdev\WpBnb\Booking\Availability;
|
|
use Magdev\WpBnb\PostTypes\Booking;
|
|
use Magdev\WpBnb\PostTypes\Building;
|
|
use Magdev\WpBnb\PostTypes\Room;
|
|
|
|
/**
|
|
* Calendar admin page class.
|
|
*/
|
|
final class Calendar {
|
|
|
|
/**
|
|
* Initialize the calendar page.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function init(): void {
|
|
add_action( 'admin_menu', array( self::class, 'register_menu' ) );
|
|
}
|
|
|
|
/**
|
|
* Register the admin menu item.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function register_menu(): void {
|
|
add_submenu_page(
|
|
'wp-bnb',
|
|
__( 'Calendar', 'wp-bnb' ),
|
|
__( 'Calendar', 'wp-bnb' ),
|
|
'edit_posts',
|
|
'wp-bnb-calendar',
|
|
array( self::class, 'render_page' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render the calendar page.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function render_page(): void {
|
|
// Get filter parameters.
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
|
$building_id = isset( $_GET['building_id'] ) ? absint( $_GET['building_id'] ) : 0;
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
|
$room_id = isset( $_GET['room_id'] ) ? absint( $_GET['room_id'] ) : 0;
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
|
$year = isset( $_GET['year'] ) ? absint( $_GET['year'] ) : (int) gmdate( 'Y' );
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
|
$month = isset( $_GET['month'] ) ? absint( $_GET['month'] ) : (int) gmdate( 'n' );
|
|
|
|
// Validate month.
|
|
if ( $month < 1 || $month > 12 ) {
|
|
$month = (int) gmdate( 'n' );
|
|
}
|
|
|
|
// Get buildings and rooms for filters.
|
|
$buildings = get_posts(
|
|
array(
|
|
'post_type' => Building::POST_TYPE,
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => -1,
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
)
|
|
);
|
|
|
|
$rooms = get_posts(
|
|
array(
|
|
'post_type' => Room::POST_TYPE,
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => -1,
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
)
|
|
);
|
|
|
|
// If room is selected, get its building.
|
|
if ( $room_id && ! $building_id ) {
|
|
$building = Room::get_building( $room_id );
|
|
if ( $building ) {
|
|
$building_id = $building->ID;
|
|
}
|
|
}
|
|
|
|
// Get rooms to display.
|
|
$display_rooms = array();
|
|
if ( $room_id ) {
|
|
$room = get_post( $room_id );
|
|
if ( $room ) {
|
|
$display_rooms[] = $room;
|
|
}
|
|
} elseif ( $building_id ) {
|
|
$display_rooms = Room::get_rooms_for_building( $building_id );
|
|
} else {
|
|
$display_rooms = $rooms;
|
|
}
|
|
|
|
// Calculate navigation dates.
|
|
$prev_month = $month === 1 ? 12 : $month - 1;
|
|
$prev_year = $month === 1 ? $year - 1 : $year;
|
|
$next_month = $month === 12 ? 1 : $month + 1;
|
|
$next_year = $month === 12 ? $year + 1 : $year;
|
|
|
|
$month_name = gmdate( 'F', mktime( 0, 0, 0, $month, 1, $year ) );
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php esc_html_e( 'Availability Calendar', 'wp-bnb' ); ?></h1>
|
|
|
|
<div class="bnb-calendar-container">
|
|
<!-- Calendar Header -->
|
|
<div class="bnb-calendar-header">
|
|
<div class="bnb-calendar-nav">
|
|
<a href="<?php echo esc_url( self::get_calendar_url( $prev_year, $prev_month, $building_id, $room_id ) ); ?>"
|
|
class="button">
|
|
« <?php esc_html_e( 'Previous', 'wp-bnb' ); ?>
|
|
</a>
|
|
<a href="<?php echo esc_url( self::get_calendar_url( (int) gmdate( 'Y' ), (int) gmdate( 'n' ), $building_id, $room_id ) ); ?>"
|
|
class="button">
|
|
<?php esc_html_e( 'Today', 'wp-bnb' ); ?>
|
|
</a>
|
|
<a href="<?php echo esc_url( self::get_calendar_url( $next_year, $next_month, $building_id, $room_id ) ); ?>"
|
|
class="button">
|
|
<?php esc_html_e( 'Next', 'wp-bnb' ); ?> »
|
|
</a>
|
|
</div>
|
|
<h2><?php echo esc_html( $month_name . ' ' . $year ); ?></h2>
|
|
<div></div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="bnb-calendar-filters">
|
|
<form method="get" action="">
|
|
<input type="hidden" name="page" value="wp-bnb-calendar">
|
|
<input type="hidden" name="year" value="<?php echo esc_attr( $year ); ?>">
|
|
<input type="hidden" name="month" value="<?php echo esc_attr( $month ); ?>">
|
|
|
|
<label>
|
|
<?php esc_html_e( 'Building:', 'wp-bnb' ); ?>
|
|
<select name="building_id" onchange="this.form.submit()">
|
|
<option value=""><?php esc_html_e( 'All Buildings', 'wp-bnb' ); ?></option>
|
|
<?php foreach ( $buildings as $building ) : ?>
|
|
<option value="<?php echo esc_attr( $building->ID ); ?>" <?php selected( $building_id, $building->ID ); ?>>
|
|
<?php echo esc_html( $building->post_title ); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</label>
|
|
|
|
<label>
|
|
<?php esc_html_e( 'Room:', 'wp-bnb' ); ?>
|
|
<select name="room_id" onchange="this.form.submit()">
|
|
<option value=""><?php esc_html_e( 'All Rooms', 'wp-bnb' ); ?></option>
|
|
<?php foreach ( $rooms as $room ) : ?>
|
|
<?php
|
|
$room_building = Room::get_building( $room->ID );
|
|
$room_label = $room->post_title;
|
|
if ( $room_building ) {
|
|
$room_label .= ' (' . $room_building->post_title . ')';
|
|
}
|
|
?>
|
|
<option value="<?php echo esc_attr( $room->ID ); ?>" <?php selected( $room_id, $room->ID ); ?>>
|
|
<?php echo esc_html( $room_label ); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</label>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Calendar Grid -->
|
|
<div class="bnb-calendar-grid">
|
|
<?php if ( empty( $display_rooms ) ) : ?>
|
|
<div class="bnb-no-rooms">
|
|
<span class="dashicons dashicons-calendar-alt"></span>
|
|
<p><?php esc_html_e( 'No rooms found. Please add rooms first.', 'wp-bnb' ); ?></p>
|
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Room::POST_TYPE ) ); ?>" class="button button-primary">
|
|
<?php esc_html_e( 'Add Room', 'wp-bnb' ); ?>
|
|
</a>
|
|
</div>
|
|
<?php else : ?>
|
|
<?php self::render_calendar_table( $display_rooms, $year, $month, (bool) $room_id ); ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Legend -->
|
|
<div class="bnb-calendar-legend">
|
|
<div class="bnb-calendar-legend-item">
|
|
<span class="bnb-calendar-legend-color available"></span>
|
|
<?php esc_html_e( 'Available', 'wp-bnb' ); ?>
|
|
</div>
|
|
<div class="bnb-calendar-legend-item">
|
|
<span class="bnb-calendar-legend-color pending"></span>
|
|
<?php esc_html_e( 'Pending', 'wp-bnb' ); ?>
|
|
</div>
|
|
<div class="bnb-calendar-legend-item">
|
|
<span class="bnb-calendar-legend-color confirmed"></span>
|
|
<?php esc_html_e( 'Confirmed', 'wp-bnb' ); ?>
|
|
</div>
|
|
<div class="bnb-calendar-legend-item">
|
|
<span class="bnb-calendar-legend-color checked-in"></span>
|
|
<?php esc_html_e( 'Checked In', 'wp-bnb' ); ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render the calendar table.
|
|
*
|
|
* @param array $rooms Rooms to display.
|
|
* @param int $year Year.
|
|
* @param int $month Month.
|
|
* @param bool $single_room Whether showing single room (more detail).
|
|
* @return void
|
|
*/
|
|
private static function render_calendar_table( array $rooms, int $year, int $month, bool $single_room = false ): void {
|
|
$days_in_month = (int) gmdate( 't', mktime( 0, 0, 0, $month, 1, $year ) );
|
|
$today = gmdate( 'Y-m-d' );
|
|
|
|
$class = $single_room ? 'bnb-calendar-table bnb-calendar-single-room' : 'bnb-calendar-table';
|
|
?>
|
|
<table class="<?php echo esc_attr( $class ); ?>">
|
|
<thead>
|
|
<tr>
|
|
<th class="room-header"><?php esc_html_e( 'Room', 'wp-bnb' ); ?></th>
|
|
<?php for ( $day = 1; $day <= $days_in_month; $day++ ) : ?>
|
|
<?php
|
|
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
|
$day_of_week = gmdate( 'D', strtotime( $date_str ) );
|
|
$is_weekend = in_array( gmdate( 'N', strtotime( $date_str ) ), array( 6, 7 ), true );
|
|
?>
|
|
<th class="<?php echo $is_weekend ? 'weekend' : ''; ?>">
|
|
<?php echo esc_html( $day ); ?><br>
|
|
<small><?php echo esc_html( $day_of_week ); ?></small>
|
|
</th>
|
|
<?php endfor; ?>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ( $rooms as $room ) : ?>
|
|
<?php
|
|
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
|
$booked_dates = Availability::get_booked_dates( $room->ID, $year, $month );
|
|
?>
|
|
<tr>
|
|
<td class="bnb-calendar-room">
|
|
<a href="<?php echo esc_url( get_edit_post_link( $room->ID ) ); ?>">
|
|
<?php echo esc_html( $room->post_title ); ?>
|
|
</a>
|
|
<?php if ( $room_number ) : ?>
|
|
<br><small>#<?php echo esc_html( $room_number ); ?></small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<?php for ( $day = 1; $day <= $days_in_month; $day++ ) : ?>
|
|
<?php
|
|
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
|
$is_past = $date_str < $today;
|
|
$is_today = $date_str === $today;
|
|
$is_booked = isset( $booked_dates[ $date_str ] );
|
|
|
|
$classes = array( 'bnb-calendar-day' );
|
|
$title = '';
|
|
$booking_id = 0;
|
|
|
|
if ( $is_past ) {
|
|
$classes[] = 'past';
|
|
}
|
|
if ( $is_today ) {
|
|
$classes[] = 'today';
|
|
}
|
|
|
|
if ( $is_booked ) {
|
|
$booking_data = $booked_dates[ $date_str ];
|
|
$booking_id = $booking_data['booking_id'];
|
|
$classes[] = 'booked';
|
|
$classes[] = 'status-' . $booking_data['status'];
|
|
|
|
if ( $booking_data['is_start'] ) {
|
|
$classes[] = 'booked-start';
|
|
}
|
|
if ( $booking_data['is_end'] ) {
|
|
$classes[] = 'booked-end';
|
|
}
|
|
|
|
$title = sprintf(
|
|
/* translators: 1: Booking reference, 2: Guest name, 3: Check-in date, 4: Check-out date */
|
|
__( '%1$s - %2$s (%3$s to %4$s)', 'wp-bnb' ),
|
|
$booking_data['reference'],
|
|
$booking_data['guest'],
|
|
wp_date( get_option( 'date_format' ), strtotime( $booking_data['check_in'] ) ),
|
|
wp_date( get_option( 'date_format' ), strtotime( $booking_data['check_out'] ) )
|
|
);
|
|
} elseif ( ! $is_past ) {
|
|
$classes[] = 'available';
|
|
}
|
|
?>
|
|
<td class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
|
|
<?php if ( $booking_id ) : ?>
|
|
data-booking-id="<?php echo esc_attr( $booking_id ); ?>"
|
|
<?php endif; ?>
|
|
<?php if ( $title ) : ?>
|
|
title="<?php echo esc_attr( $title ); ?>"
|
|
<?php endif; ?>>
|
|
<?php if ( $single_room && $is_booked && $booking_data['is_start'] ) : ?>
|
|
<span class="guest-name"><?php echo esc_html( $booking_data['guest'] ); ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<?php endfor; ?>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Get calendar URL with parameters.
|
|
*
|
|
* @param int $year Year.
|
|
* @param int $month Month.
|
|
* @param int $building_id Building ID (optional).
|
|
* @param int $room_id Room ID (optional).
|
|
* @return string URL.
|
|
*/
|
|
private static function get_calendar_url( int $year, int $month, int $building_id = 0, int $room_id = 0 ): string {
|
|
$args = array(
|
|
'page' => 'wp-bnb-calendar',
|
|
'year' => $year,
|
|
'month' => $month,
|
|
);
|
|
|
|
if ( $building_id ) {
|
|
$args['building_id'] = $building_id;
|
|
}
|
|
|
|
if ( $room_id ) {
|
|
$args['room_id'] = $room_id;
|
|
}
|
|
|
|
return add_query_arg( $args, admin_url( 'admin.php' ) );
|
|
}
|
|
}
|