Add booking system with calendar and email notifications (v0.3.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m5s
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>
This commit is contained in:
359
src/Admin/Calendar.php
Normal file
359
src/Admin/Calendar.php
Normal file
@@ -0,0 +1,359 @@
|
||||
<?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' ) );
|
||||
}
|
||||
}
|
||||
444
src/Booking/Availability.php
Normal file
444
src/Booking/Availability.php
Normal file
@@ -0,0 +1,444 @@
|
||||
<?php
|
||||
/**
|
||||
* Availability checker.
|
||||
*
|
||||
* Handles availability checks and calendar data for rooms and bookings.
|
||||
*
|
||||
* @package Magdev\WpBnb\Booking
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Booking;
|
||||
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
|
||||
/**
|
||||
* Availability class.
|
||||
*/
|
||||
final class Availability {
|
||||
|
||||
/**
|
||||
* Check if a room is available for a date range.
|
||||
*
|
||||
* @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_booking Booking ID to exclude (for editing).
|
||||
* @return bool True if available, false if conflicts exist.
|
||||
*/
|
||||
public static function is_available( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): bool {
|
||||
return ! Booking::has_conflict( $room_id, $check_in, $check_out, $exclude_booking );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all booked dates for a room in a specific month.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @param int $year Year (e.g., 2024).
|
||||
* @param int $month Month (1-12).
|
||||
* @return array<string, array> Array of dates (Y-m-d) with booking info.
|
||||
*/
|
||||
public static function get_booked_dates( int $room_id, int $year, int $month ): array {
|
||||
$month_start = sprintf( '%04d-%02d-01', $year, $month );
|
||||
$month_end = gmdate( 'Y-m-t', strtotime( $month_start ) );
|
||||
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => Booking::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_bnb_booking_room_id',
|
||||
'value' => $room_id,
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_status',
|
||||
'value' => 'cancelled',
|
||||
'compare' => '!=',
|
||||
),
|
||||
// Booking overlaps with month.
|
||||
array(
|
||||
'key' => '_bnb_booking_check_in',
|
||||
'value' => $month_end,
|
||||
'compare' => '<=',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_check_out',
|
||||
'value' => $month_start,
|
||||
'compare' => '>=',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$booked_dates = array();
|
||||
|
||||
foreach ( $bookings as $booking ) {
|
||||
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||
$guest = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||
|
||||
// Iterate through each night of the booking.
|
||||
$current = new \DateTimeImmutable( $check_in );
|
||||
$end = new \DateTimeImmutable( $check_out );
|
||||
|
||||
while ( $current < $end ) {
|
||||
$date_str = $current->format( 'Y-m-d' );
|
||||
|
||||
// Only include dates within the requested month.
|
||||
if ( $current->format( 'Y-m' ) === sprintf( '%04d-%02d', $year, $month ) ) {
|
||||
$booked_dates[ $date_str ] = array(
|
||||
'booking_id' => $booking->ID,
|
||||
'reference' => $booking->post_title,
|
||||
'guest' => $guest,
|
||||
'status' => $status,
|
||||
'check_in' => $check_in,
|
||||
'check_out' => $check_out,
|
||||
'is_start' => $date_str === $check_in,
|
||||
'is_end' => $current->modify( '+1 day' )->format( 'Y-m-d' ) === $check_out,
|
||||
);
|
||||
}
|
||||
|
||||
$current = $current->modify( '+1 day' );
|
||||
}
|
||||
}
|
||||
|
||||
return $booked_dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calendar data for a room for a specific month.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @param int $year Year.
|
||||
* @param int $month Month (1-12).
|
||||
* @return array Calendar data including days and bookings.
|
||||
*/
|
||||
public static function get_calendar_data( int $room_id, int $year, int $month ): array {
|
||||
$month_start = new \DateTimeImmutable( sprintf( '%04d-%02d-01', $year, $month ) );
|
||||
$days_in_month = (int) $month_start->format( 't' );
|
||||
$first_day_of_week = (int) $month_start->format( 'w' ); // 0 = Sunday.
|
||||
|
||||
$booked_dates = self::get_booked_dates( $room_id, $year, $month );
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
$days = array();
|
||||
for ( $day = 1; $day <= $days_in_month; $day++ ) {
|
||||
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
||||
$is_booked = isset( $booked_dates[ $date_str ] );
|
||||
|
||||
$days[ $day ] = array(
|
||||
'date' => $date_str,
|
||||
'day' => $day,
|
||||
'is_booked' => $is_booked,
|
||||
'is_past' => $date_str < $today,
|
||||
'is_today' => $date_str === $today,
|
||||
'booking' => $is_booked ? $booked_dates[ $date_str ] : null,
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'room_id' => $room_id,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'month_name' => $month_start->format( 'F' ),
|
||||
'days_in_month' => $days_in_month,
|
||||
'first_day_of_week' => $first_day_of_week,
|
||||
'days' => $days,
|
||||
'prev_month' => array(
|
||||
'year' => $month === 1 ? $year - 1 : $year,
|
||||
'month' => $month === 1 ? 12 : $month - 1,
|
||||
),
|
||||
'next_month' => array(
|
||||
'year' => $month === 12 ? $year + 1 : $year,
|
||||
'month' => $month === 12 ? 1 : $month + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get availability summary for a building (all rooms).
|
||||
*
|
||||
* @param int $building_id Building post ID.
|
||||
* @param int $year Year.
|
||||
* @param int $month Month (1-12).
|
||||
* @return array Availability data for all rooms in the building.
|
||||
*/
|
||||
public static function get_building_availability( int $building_id, int $year, int $month ): array {
|
||||
$rooms = Room::get_rooms_for_building( $building_id );
|
||||
$data = array();
|
||||
|
||||
foreach ( $rooms as $room ) {
|
||||
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
||||
$data[ $room->ID ] = array(
|
||||
'room_id' => $room->ID,
|
||||
'room_name' => $room->post_title,
|
||||
'room_number' => $room_number,
|
||||
'calendar' => self::get_calendar_data( $room->ID, $year, $month ),
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conflicts for a proposed booking.
|
||||
*
|
||||
* @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_booking Booking ID to exclude.
|
||||
* @return array<\WP_Post> Array of conflicting booking posts.
|
||||
*/
|
||||
public static function get_conflicts( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): array {
|
||||
$args = array(
|
||||
'post_type' => Booking::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_bnb_booking_room_id',
|
||||
'value' => $room_id,
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_status',
|
||||
'value' => 'cancelled',
|
||||
'compare' => '!=',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_check_in',
|
||||
'value' => $check_out,
|
||||
'compare' => '<',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_check_out',
|
||||
'value' => $check_in,
|
||||
'compare' => '>',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if ( $exclude_booking ) {
|
||||
$args['post__not_in'] = array( $exclude_booking );
|
||||
}
|
||||
|
||||
return get_posts( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get availability check result with pricing.
|
||||
*
|
||||
* @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_booking Booking ID to exclude.
|
||||
* @return array Result with availability, pricing, and conflicts.
|
||||
*/
|
||||
public static function check_availability_with_price( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): array {
|
||||
$conflicts = self::get_conflicts( $room_id, $check_in, $check_out, $exclude_booking );
|
||||
$available = empty( $conflicts );
|
||||
|
||||
$result = array(
|
||||
'available' => $available,
|
||||
'room_id' => $room_id,
|
||||
'check_in' => $check_in,
|
||||
'check_out' => $check_out,
|
||||
'nights' => Booking::calculate_nights( $check_in, $check_out ),
|
||||
'conflicts' => array(),
|
||||
);
|
||||
|
||||
if ( ! $available ) {
|
||||
foreach ( $conflicts as $conflict ) {
|
||||
$result['conflicts'][] = array(
|
||||
'booking_id' => $conflict->ID,
|
||||
'reference' => $conflict->post_title,
|
||||
'check_in' => get_post_meta( $conflict->ID, '_bnb_booking_check_in', true ),
|
||||
'check_out' => get_post_meta( $conflict->ID, '_bnb_booking_check_out', true ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate price if available.
|
||||
if ( $available ) {
|
||||
try {
|
||||
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||
$price = $calculator->calculate();
|
||||
$breakdown = $calculator->getBreakdown();
|
||||
|
||||
$result['price'] = $price;
|
||||
$result['price_formatted'] = Calculator::formatPrice( $price );
|
||||
$result['breakdown'] = $breakdown;
|
||||
} catch ( \Exception $e ) {
|
||||
$result['price'] = null;
|
||||
$result['price_formatted'] = null;
|
||||
$result['price_error'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upcoming bookings for a room.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @param int $limit Maximum number of bookings to return.
|
||||
* @return array<\WP_Post> Array of upcoming bookings.
|
||||
*/
|
||||
public static function get_upcoming_bookings( int $room_id, int $limit = 5 ): array {
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => Booking::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $limit,
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_bnb_booking_room_id',
|
||||
'value' => $room_id,
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_status',
|
||||
'value' => array( 'pending', 'confirmed' ),
|
||||
'compare' => 'IN',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_check_in',
|
||||
'value' => $today,
|
||||
'compare' => '>=',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
),
|
||||
'orderby' => 'meta_value',
|
||||
'meta_key' => '_bnb_booking_check_in',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current bookings (guests currently checked in).
|
||||
*
|
||||
* @param int|null $room_id Optional room ID to filter by.
|
||||
* @return array<\WP_Post> Array of current bookings.
|
||||
*/
|
||||
public static function get_current_bookings( ?int $room_id = null ): array {
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
$meta_query = array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_bnb_booking_status',
|
||||
'value' => 'checked_in',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_check_in',
|
||||
'value' => $today,
|
||||
'compare' => '<=',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_check_out',
|
||||
'value' => $today,
|
||||
'compare' => '>',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
);
|
||||
|
||||
if ( $room_id ) {
|
||||
$meta_query[] = array(
|
||||
'key' => '_bnb_booking_room_id',
|
||||
'value' => $room_id,
|
||||
);
|
||||
}
|
||||
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => Booking::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => $meta_query,
|
||||
'orderby' => 'meta_value',
|
||||
'meta_key' => '_bnb_booking_check_out',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get today's check-ins.
|
||||
*
|
||||
* @return array<\WP_Post> Array of bookings with check-in today.
|
||||
*/
|
||||
public static function get_todays_checkins(): array {
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => Booking::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_bnb_booking_status',
|
||||
'value' => 'confirmed',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_check_in',
|
||||
'value' => $today,
|
||||
'type' => 'DATE',
|
||||
),
|
||||
),
|
||||
'orderby' => 'meta_value',
|
||||
'meta_key' => '_bnb_booking_guest_name',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get today's check-outs.
|
||||
*
|
||||
* @return array<\WP_Post> Array of bookings with check-out today.
|
||||
*/
|
||||
public static function get_todays_checkouts(): array {
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => Booking::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_bnb_booking_status',
|
||||
'value' => 'checked_in',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_booking_check_out',
|
||||
'value' => $today,
|
||||
'type' => 'DATE',
|
||||
),
|
||||
),
|
||||
'orderby' => 'meta_value',
|
||||
'meta_key' => '_bnb_booking_guest_name',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
587
src/Booking/EmailNotifier.php
Normal file
587
src/Booking/EmailNotifier.php
Normal file
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
/**
|
||||
* Email notifier for bookings.
|
||||
*
|
||||
* Handles sending email notifications for booking events.
|
||||
*
|
||||
* @package Magdev\WpBnb\Booking
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Booking;
|
||||
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
|
||||
/**
|
||||
* EmailNotifier class.
|
||||
*/
|
||||
final class EmailNotifier {
|
||||
|
||||
/**
|
||||
* Initialize the email notifier.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action( 'wp_bnb_booking_status_changed', array( self::class, 'on_status_change' ), 10, 3 );
|
||||
add_action( 'save_post_' . Booking::POST_TYPE, array( self::class, 'on_booking_created' ), 20, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle status change event.
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @param string $old_status Previous status.
|
||||
* @param string $new_status New status.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_status_change( int $booking_id, string $old_status, string $new_status ): void {
|
||||
switch ( $new_status ) {
|
||||
case 'confirmed':
|
||||
self::send_guest_confirmation( $booking_id );
|
||||
self::send_admin_confirmation( $booking_id );
|
||||
break;
|
||||
|
||||
case 'cancelled':
|
||||
self::send_cancellation( $booking_id );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle booking created event.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_booking_created( int $post_id, \WP_Post $post ): void {
|
||||
// Skip if autosave or revision.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( wp_is_post_revision( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a new booking (created in the last 30 seconds).
|
||||
$created = get_post_time( 'U', true, $post_id );
|
||||
if ( time() - $created > 30 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've already sent this notification.
|
||||
$sent = get_post_meta( $post_id, '_bnb_booking_new_email_sent', true );
|
||||
if ( $sent ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as sent before sending to prevent duplicates.
|
||||
update_post_meta( $post_id, '_bnb_booking_new_email_sent', '1' );
|
||||
|
||||
// Send admin notification for new booking.
|
||||
self::send_admin_new_booking( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send new booking notification to admin.
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return bool Whether email was sent.
|
||||
*/
|
||||
public static function send_admin_new_booking( int $booking_id ): bool {
|
||||
$booking = get_post( $booking_id );
|
||||
if ( ! $booking ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = self::get_booking_data( $booking_id );
|
||||
$to = get_option( 'admin_email' );
|
||||
$subject = sprintf(
|
||||
/* translators: 1: Site name, 2: Booking reference */
|
||||
__( '[%1$s] New Booking: %2$s', 'wp-bnb' ),
|
||||
get_bloginfo( 'name' ),
|
||||
$data['booking_reference']
|
||||
);
|
||||
|
||||
$message = self::get_email_template( 'admin-new-booking', $data );
|
||||
|
||||
return self::send_email( $to, $subject, $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send confirmation email to guest.
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return bool Whether email was sent.
|
||||
*/
|
||||
public static function send_guest_confirmation( int $booking_id ): bool {
|
||||
$data = self::get_booking_data( $booking_id );
|
||||
|
||||
if ( empty( $data['guest_email'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subject = sprintf(
|
||||
/* translators: 1: Site name, 2: Booking reference */
|
||||
__( '[%1$s] Booking Confirmed: %2$s', 'wp-bnb' ),
|
||||
get_bloginfo( 'name' ),
|
||||
$data['booking_reference']
|
||||
);
|
||||
|
||||
$message = self::get_email_template( 'booking-confirmed', $data );
|
||||
|
||||
return self::send_email( $data['guest_email'], $subject, $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send confirmation notification to admin.
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return bool Whether email was sent.
|
||||
*/
|
||||
public static function send_admin_confirmation( int $booking_id ): bool {
|
||||
$data = self::get_booking_data( $booking_id );
|
||||
$to = get_option( 'admin_email' );
|
||||
$subject = sprintf(
|
||||
/* translators: 1: Site name, 2: Booking reference */
|
||||
__( '[%1$s] Booking Confirmed: %2$s', 'wp-bnb' ),
|
||||
get_bloginfo( 'name' ),
|
||||
$data['booking_reference']
|
||||
);
|
||||
|
||||
$message = self::get_email_template( 'admin-booking-confirmed', $data );
|
||||
|
||||
return self::send_email( $to, $subject, $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send cancellation email.
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return bool Whether emails were sent.
|
||||
*/
|
||||
public static function send_cancellation( int $booking_id ): bool {
|
||||
$data = self::get_booking_data( $booking_id );
|
||||
$result = true;
|
||||
|
||||
// Send to admin.
|
||||
$admin_subject = sprintf(
|
||||
/* translators: 1: Site name, 2: Booking reference */
|
||||
__( '[%1$s] Booking Cancelled: %2$s', 'wp-bnb' ),
|
||||
get_bloginfo( 'name' ),
|
||||
$data['booking_reference']
|
||||
);
|
||||
|
||||
$admin_message = self::get_email_template( 'admin-booking-cancelled', $data );
|
||||
$result = self::send_email( get_option( 'admin_email' ), $admin_subject, $admin_message ) && $result;
|
||||
|
||||
// Send to guest if email exists.
|
||||
if ( ! empty( $data['guest_email'] ) ) {
|
||||
$guest_subject = sprintf(
|
||||
/* translators: 1: Site name, 2: Booking reference */
|
||||
__( '[%1$s] Booking Cancelled: %2$s', 'wp-bnb' ),
|
||||
get_bloginfo( 'name' ),
|
||||
$data['booking_reference']
|
||||
);
|
||||
|
||||
$guest_message = self::get_email_template( 'booking-cancelled', $data );
|
||||
$result = self::send_email( $data['guest_email'], $guest_subject, $guest_message ) && $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get booking data for email templates.
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return array Booking data.
|
||||
*/
|
||||
private static function get_booking_data( int $booking_id ): array {
|
||||
$booking = get_post( $booking_id );
|
||||
$room = Booking::get_room( $booking_id );
|
||||
$building = Booking::get_building( $booking_id );
|
||||
|
||||
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||
$price = get_post_meta( $booking_id, '_bnb_booking_calculated_price', true );
|
||||
$adults = get_post_meta( $booking_id, '_bnb_booking_adults', true );
|
||||
$children = get_post_meta( $booking_id, '_bnb_booking_children', true );
|
||||
|
||||
$nights = 0;
|
||||
if ( $check_in && $check_out ) {
|
||||
$nights = Booking::calculate_nights( $check_in, $check_out );
|
||||
}
|
||||
|
||||
$statuses = Booking::get_booking_statuses();
|
||||
|
||||
return array(
|
||||
'booking_id' => $booking_id,
|
||||
'booking_reference' => $booking ? $booking->post_title : '',
|
||||
'guest_name' => get_post_meta( $booking_id, '_bnb_booking_guest_name', true ),
|
||||
'guest_email' => get_post_meta( $booking_id, '_bnb_booking_guest_email', true ),
|
||||
'guest_phone' => get_post_meta( $booking_id, '_bnb_booking_guest_phone', true ),
|
||||
'guest_notes' => get_post_meta( $booking_id, '_bnb_booking_guest_notes', true ),
|
||||
'adults' => $adults ?: 1,
|
||||
'children' => $children ?: 0,
|
||||
'room_name' => $room ? $room->post_title : '',
|
||||
'room_id' => $room ? $room->ID : 0,
|
||||
'building_name' => $building ? $building->post_title : '',
|
||||
'building_id' => $building ? $building->ID : 0,
|
||||
'check_in_date' => $check_in ? wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) : '',
|
||||
'check_out_date' => $check_out ? wp_date( get_option( 'date_format' ), strtotime( $check_out ) ) : '',
|
||||
'check_in_raw' => $check_in,
|
||||
'check_out_raw' => $check_out,
|
||||
'nights' => $nights,
|
||||
'total_price' => $price ? Calculator::formatPrice( (float) $price ) : '',
|
||||
'status' => $statuses[ $status ] ?? $status,
|
||||
'status_raw' => $status,
|
||||
'site_name' => get_bloginfo( 'name' ),
|
||||
'site_url' => home_url(),
|
||||
'admin_email' => get_option( 'admin_email' ),
|
||||
'booking_url' => admin_url( 'post.php?post=' . $booking_id . '&action=edit' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email template with placeholders replaced.
|
||||
*
|
||||
* @param string $template_name Template name.
|
||||
* @param array $data Template data.
|
||||
* @return string HTML email content.
|
||||
*/
|
||||
private static function get_email_template( string $template_name, array $data ): string {
|
||||
$template = self::get_template_content( $template_name );
|
||||
|
||||
// Replace placeholders.
|
||||
foreach ( $data as $key => $value ) {
|
||||
$template = str_replace( '{' . $key . '}', (string) $value, $template );
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template content.
|
||||
*
|
||||
* @param string $template_name Template name.
|
||||
* @return string Template HTML.
|
||||
*/
|
||||
private static function get_template_content( string $template_name ): string {
|
||||
// Built-in templates. Could be extended to load from files.
|
||||
$templates = array(
|
||||
'admin-new-booking' => self::template_admin_new_booking(),
|
||||
'booking-confirmed' => self::template_booking_confirmed(),
|
||||
'admin-booking-confirmed' => self::template_admin_booking_confirmed(),
|
||||
'booking-cancelled' => self::template_booking_cancelled(),
|
||||
'admin-booking-cancelled' => self::template_admin_booking_cancelled(),
|
||||
);
|
||||
|
||||
return $templates[ $template_name ] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send HTML email.
|
||||
*
|
||||
* @param string $to Recipient email.
|
||||
* @param string $subject Email subject.
|
||||
* @param string $message HTML message.
|
||||
* @return bool Whether email was sent.
|
||||
*/
|
||||
private static function send_email( string $to, string $subject, string $message ): bool {
|
||||
$headers = array(
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'From: ' . get_bloginfo( 'name' ) . ' <' . get_option( 'admin_email' ) . '>',
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter email recipients.
|
||||
*
|
||||
* @param string $to Recipient email.
|
||||
* @param string $subject Email subject.
|
||||
* @param string $message Email message.
|
||||
*/
|
||||
$to = apply_filters( 'wp_bnb_booking_email_recipients', $to, $subject, $message );
|
||||
|
||||
/**
|
||||
* Filter email subject.
|
||||
*
|
||||
* @param string $subject Email subject.
|
||||
*/
|
||||
$subject = apply_filters( 'wp_bnb_booking_email_subject', $subject );
|
||||
|
||||
/**
|
||||
* Filter email content.
|
||||
*
|
||||
* @param string $message Email message.
|
||||
*/
|
||||
$message = apply_filters( 'wp_bnb_booking_email_content', $message );
|
||||
|
||||
return wp_mail( $to, $subject, $message, $headers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base email styles.
|
||||
*
|
||||
* @return string CSS styles.
|
||||
*/
|
||||
private static function get_email_styles(): string {
|
||||
return '
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 14px; line-height: 1.6; color: #333; }
|
||||
.email-container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.email-header { background: #135e96; color: #fff; padding: 20px; text-align: center; }
|
||||
.email-header h1 { margin: 0; font-size: 24px; }
|
||||
.email-body { background: #fff; padding: 30px; border: 1px solid #ddd; }
|
||||
.email-footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
|
||||
.booking-details { background: #f9f9f9; padding: 15px; margin: 20px 0; border-left: 4px solid #135e96; }
|
||||
.booking-details h3 { margin-top: 0; color: #135e96; }
|
||||
.detail-row { margin: 8px 0; }
|
||||
.detail-label { font-weight: 600; display: inline-block; min-width: 120px; }
|
||||
.btn { display: inline-block; padding: 10px 20px; background: #135e96; color: #fff; text-decoration: none; border-radius: 4px; margin-top: 15px; }
|
||||
.status-badge { display: inline-block; padding: 4px 10px; border-radius: 3px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
|
||||
.status-pending { background: #dba617; color: #fff; }
|
||||
.status-confirmed { background: #00a32a; color: #fff; }
|
||||
.status-cancelled { background: #d63638; color: #fff; }
|
||||
';
|
||||
}
|
||||
|
||||
/**
|
||||
* Template: Admin new booking notification.
|
||||
*
|
||||
* @return string Template HTML.
|
||||
*/
|
||||
private static function template_admin_new_booking(): string {
|
||||
$styles = self::get_email_styles();
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{$styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>New Booking Received</h1>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>A new booking has been created and is awaiting confirmation.</p>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Booking Details</h3>
|
||||
<div class="detail-row"><span class="detail-label">Reference:</span> {booking_reference}</div>
|
||||
<div class="detail-row"><span class="detail-label">Status:</span> <span class="status-badge status-{status_raw}">{status}</span></div>
|
||||
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Building:</span> {building_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Check-in:</span> {check_in_date}</div>
|
||||
<div class="detail-row"><span class="detail-label">Check-out:</span> {check_out_date}</div>
|
||||
<div class="detail-row"><span class="detail-label">Nights:</span> {nights}</div>
|
||||
<div class="detail-row"><span class="detail-label">Total:</span> {total_price}</div>
|
||||
</div>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Guest Information</h3>
|
||||
<div class="detail-row"><span class="detail-label">Name:</span> {guest_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Email:</span> {guest_email}</div>
|
||||
<div class="detail-row"><span class="detail-label">Phone:</span> {guest_phone}</div>
|
||||
<div class="detail-row"><span class="detail-label">Adults:</span> {adults}</div>
|
||||
<div class="detail-row"><span class="detail-label">Children:</span> {children}</div>
|
||||
</div>
|
||||
|
||||
<a href="{booking_url}" class="btn">View Booking</a>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This email was sent from {site_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template: Guest booking confirmed.
|
||||
*
|
||||
* @return string Template HTML.
|
||||
*/
|
||||
private static function template_booking_confirmed(): string {
|
||||
$styles = self::get_email_styles();
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{$styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>Booking Confirmed</h1>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Dear {guest_name},</p>
|
||||
|
||||
<p>Great news! Your booking has been confirmed. We look forward to welcoming you.</p>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Your Booking Details</h3>
|
||||
<div class="detail-row"><span class="detail-label">Confirmation:</span> {booking_reference}</div>
|
||||
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Location:</span> {building_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Check-in:</span> {check_in_date}</div>
|
||||
<div class="detail-row"><span class="detail-label">Check-out:</span> {check_out_date}</div>
|
||||
<div class="detail-row"><span class="detail-label">Duration:</span> {nights} nights</div>
|
||||
<div class="detail-row"><span class="detail-label">Guests:</span> {adults} adults, {children} children</div>
|
||||
<div class="detail-row"><span class="detail-label">Total:</span> <strong>{total_price}</strong></div>
|
||||
</div>
|
||||
|
||||
<p>If you have any questions or need to make changes to your reservation, please contact us at {admin_email}.</p>
|
||||
|
||||
<p>Thank you for choosing us!</p>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>{site_name}<br>{site_url}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template: Admin booking confirmed.
|
||||
*
|
||||
* @return string Template HTML.
|
||||
*/
|
||||
private static function template_admin_booking_confirmed(): string {
|
||||
$styles = self::get_email_styles();
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{$styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>Booking Confirmed</h1>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Booking {booking_reference} has been confirmed.</p>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Booking Summary</h3>
|
||||
<div class="detail-row"><span class="detail-label">Guest:</span> {guest_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||
<div class="detail-row"><span class="detail-label">Total:</span> {total_price}</div>
|
||||
</div>
|
||||
|
||||
<a href="{booking_url}" class="btn">View Booking</a>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This email was sent from {site_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template: Guest booking cancelled.
|
||||
*
|
||||
* @return string Template HTML.
|
||||
*/
|
||||
private static function template_booking_cancelled(): string {
|
||||
$styles = self::get_email_styles();
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{$styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header" style="background: #d63638;">
|
||||
<h1>Booking Cancelled</h1>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Dear {guest_name},</p>
|
||||
|
||||
<p>We're writing to confirm that your booking has been cancelled.</p>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Cancelled Booking</h3>
|
||||
<div class="detail-row"><span class="detail-label">Reference:</span> {booking_reference}</div>
|
||||
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||
</div>
|
||||
|
||||
<p>If you have any questions or would like to make a new reservation, please contact us at {admin_email}.</p>
|
||||
|
||||
<p>We hope to welcome you in the future.</p>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>{site_name}<br>{site_url}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template: Admin booking cancelled.
|
||||
*
|
||||
* @return string Template HTML.
|
||||
*/
|
||||
private static function template_admin_booking_cancelled(): string {
|
||||
$styles = self::get_email_styles();
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{$styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header" style="background: #d63638;">
|
||||
<h1>Booking Cancelled</h1>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Booking {booking_reference} has been cancelled.</p>
|
||||
|
||||
<div class="booking-details">
|
||||
<h3>Cancelled Booking</h3>
|
||||
<div class="detail-row"><span class="detail-label">Guest:</span> {guest_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Email:</span> {guest_email}</div>
|
||||
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||
</div>
|
||||
|
||||
<a href="{booking_url}" class="btn">View Booking</a>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This email was sent from {site_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,12 @@ declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb;
|
||||
|
||||
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
||||
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||
use Magdev\WpBnb\Booking\Availability;
|
||||
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Pricing\Season;
|
||||
@@ -87,6 +91,7 @@ final class Plugin {
|
||||
private function register_post_types(): void {
|
||||
Building::init();
|
||||
Room::init();
|
||||
Booking::init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,6 +137,15 @@ final class Plugin {
|
||||
|
||||
// Initialize seasons admin page.
|
||||
SeasonsAdmin::init();
|
||||
|
||||
// Initialize calendar admin page.
|
||||
CalendarAdmin::init();
|
||||
|
||||
// Initialize email notifier.
|
||||
EmailNotifier::init();
|
||||
|
||||
// Register AJAX handlers.
|
||||
add_action( 'wp_ajax_wp_bnb_check_availability', array( $this, 'ajax_check_availability' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +181,7 @@ final class Plugin {
|
||||
|
||||
// Check if we're on plugin pages or editing our custom post types.
|
||||
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
||||
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE ), true );
|
||||
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE ), true );
|
||||
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
||||
|
||||
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
||||
@@ -205,15 +219,22 @@ final class Plugin {
|
||||
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
||||
'postType' => $post_type,
|
||||
'i18n' => array(
|
||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
||||
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
||||
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
||||
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
||||
'increase' => __( 'increase', 'wp-bnb' ),
|
||||
'discount' => __( 'discount', 'wp-bnb' ),
|
||||
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
||||
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
||||
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
||||
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
||||
'increase' => __( 'increase', 'wp-bnb' ),
|
||||
'discount' => __( 'discount', 'wp-bnb' ),
|
||||
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
||||
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
||||
'available' => __( 'Available', 'wp-bnb' ),
|
||||
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
||||
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
||||
'nights' => __( 'nights', 'wp-bnb' ),
|
||||
'night' => __( 'night', 'wp-bnb' ),
|
||||
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
@@ -817,6 +838,43 @@ final class Plugin {
|
||||
settings_errors( 'wp_bnb_settings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for checking room availability.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_check_availability(): void {
|
||||
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ) )
|
||||
);
|
||||
}
|
||||
|
||||
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||
$check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '';
|
||||
$check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '';
|
||||
$exclude = isset( $_POST['exclude_booking'] ) ? absint( $_POST['exclude_booking'] ) : null;
|
||||
|
||||
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||
);
|
||||
}
|
||||
|
||||
// Validate dates.
|
||||
if ( strtotime( $check_out ) <= strtotime( $check_in ) ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'Check-out date must be after check-in date.', 'wp-bnb' ) )
|
||||
);
|
||||
}
|
||||
|
||||
$result = Availability::check_availability_with_price( $room_id, $check_in, $check_out, $exclude );
|
||||
|
||||
wp_send_json_success( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twig environment.
|
||||
*
|
||||
|
||||
1137
src/PostTypes/Booking.php
Normal file
1137
src/PostTypes/Booking.php
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user