Implement Phase 9: Prometheus Metrics (v0.9.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m8s
All checks were successful
Create Release Package / build-release (push) Successful in 1m8s
- Add Prometheus metrics integration via wp-prometheus hooks - Create Grafana dashboard JSON with 24 panels - Add metrics settings tab with enable/disable toggle - Expose inventory, booking, occupancy, revenue, and guest metrics Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
877
src/Integration/Prometheus.php
Normal file
877
src/Integration/Prometheus.php
Normal file
@@ -0,0 +1,877 @@
|
||||
<?php
|
||||
/**
|
||||
* Prometheus Metrics Integration.
|
||||
*
|
||||
* Provides meaningful metrics for monitoring BnB operations.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration;
|
||||
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Guest;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\PostTypes\Service;
|
||||
|
||||
/**
|
||||
* Prometheus Metrics Integration class.
|
||||
*
|
||||
* Exposes BnB metrics via the wp-prometheus plugin.
|
||||
*/
|
||||
class Prometheus {
|
||||
|
||||
/**
|
||||
* Option key for enabling metrics.
|
||||
*/
|
||||
public const OPTION_ENABLED = 'wp_bnb_metrics_enabled';
|
||||
|
||||
/**
|
||||
* Initialize the Prometheus integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Only hook if metrics are enabled.
|
||||
if ( ! self::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hook into wp-prometheus collector.
|
||||
add_action( 'wp_prometheus_collect_metrics', array( self::class, 'collect_metrics' ) );
|
||||
|
||||
// Register Grafana dashboard.
|
||||
add_action( 'wp_prometheus_register_dashboards', array( self::class, 'register_dashboards' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if metrics collection is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled(): bool {
|
||||
return 'yes' === get_option( self::OPTION_ENABLED, 'yes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable metrics collection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function enable(): void {
|
||||
update_option( self::OPTION_ENABLED, 'yes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable metrics collection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function disable(): void {
|
||||
update_option( self::OPTION_ENABLED, 'no' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect and register all BnB metrics.
|
||||
*
|
||||
* @param object $collector The wp-prometheus collector instance.
|
||||
* @return void
|
||||
*/
|
||||
public static function collect_metrics( $collector ): void {
|
||||
self::collect_inventory_metrics( $collector );
|
||||
self::collect_booking_metrics( $collector );
|
||||
self::collect_guest_metrics( $collector );
|
||||
self::collect_occupancy_metrics( $collector );
|
||||
self::collect_revenue_metrics( $collector );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect inventory metrics (buildings, rooms, services).
|
||||
*
|
||||
* @param object $collector The wp-prometheus collector instance.
|
||||
* @return void
|
||||
*/
|
||||
private static function collect_inventory_metrics( $collector ): void {
|
||||
// Buildings total.
|
||||
$buildings_total = wp_count_posts( Building::POST_TYPE );
|
||||
$gauge = $collector->register_gauge(
|
||||
'wp_bnb_buildings_total',
|
||||
'Total number of buildings',
|
||||
array()
|
||||
);
|
||||
$gauge->set( (int) $buildings_total->publish, array() );
|
||||
|
||||
// Rooms by status.
|
||||
$rooms_gauge = $collector->register_gauge(
|
||||
'wp_bnb_rooms_total',
|
||||
'Total number of rooms by status',
|
||||
array( 'status' )
|
||||
);
|
||||
|
||||
$room_statuses = array( 'available', 'occupied', 'maintenance', 'inactive' );
|
||||
foreach ( $room_statuses as $status ) {
|
||||
$count = self::count_rooms_by_status( $status );
|
||||
$rooms_gauge->set( $count, array( $status ) );
|
||||
}
|
||||
|
||||
// Services by status.
|
||||
$services_gauge = $collector->register_gauge(
|
||||
'wp_bnb_services_total',
|
||||
'Total number of services by status',
|
||||
array( 'status' )
|
||||
);
|
||||
|
||||
$service_statuses = array( 'active', 'inactive' );
|
||||
foreach ( $service_statuses as $status ) {
|
||||
$count = self::count_services_by_status( $status );
|
||||
$services_gauge->set( $count, array( $status ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect booking metrics.
|
||||
*
|
||||
* @param object $collector The wp-prometheus collector instance.
|
||||
* @return void
|
||||
*/
|
||||
private static function collect_booking_metrics( $collector ): void {
|
||||
// Bookings by status.
|
||||
$bookings_gauge = $collector->register_gauge(
|
||||
'wp_bnb_bookings_total',
|
||||
'Total number of bookings by status',
|
||||
array( 'status' )
|
||||
);
|
||||
|
||||
$booking_statuses = array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' );
|
||||
foreach ( $booking_statuses as $status ) {
|
||||
$count = self::count_bookings_by_status( $status );
|
||||
$bookings_gauge->set( $count, array( $status ) );
|
||||
}
|
||||
|
||||
// Today's check-ins.
|
||||
$checkins_gauge = $collector->register_gauge(
|
||||
'wp_bnb_checkins_today',
|
||||
'Number of check-ins scheduled for today',
|
||||
array()
|
||||
);
|
||||
$checkins_gauge->set( self::count_todays_checkins(), array() );
|
||||
|
||||
// Today's check-outs.
|
||||
$checkouts_gauge = $collector->register_gauge(
|
||||
'wp_bnb_checkouts_today',
|
||||
'Number of check-outs scheduled for today',
|
||||
array()
|
||||
);
|
||||
$checkouts_gauge->set( self::count_todays_checkouts(), array() );
|
||||
|
||||
// Upcoming bookings (next 7 days).
|
||||
$upcoming_gauge = $collector->register_gauge(
|
||||
'wp_bnb_bookings_upcoming_7days',
|
||||
'Number of bookings starting in the next 7 days',
|
||||
array()
|
||||
);
|
||||
$upcoming_gauge->set( self::count_upcoming_bookings( 7 ), array() );
|
||||
|
||||
// Average booking duration (nights).
|
||||
$avg_duration = $collector->register_gauge(
|
||||
'wp_bnb_booking_avg_duration_nights',
|
||||
'Average booking duration in nights',
|
||||
array()
|
||||
);
|
||||
$avg_duration->set( self::get_average_booking_duration(), array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect guest metrics.
|
||||
*
|
||||
* @param object $collector The wp-prometheus collector instance.
|
||||
* @return void
|
||||
*/
|
||||
private static function collect_guest_metrics( $collector ): void {
|
||||
// Total guests.
|
||||
$guests_total = wp_count_posts( Guest::POST_TYPE );
|
||||
$guests_gauge = $collector->register_gauge(
|
||||
'wp_bnb_guests_total',
|
||||
'Total number of registered guests',
|
||||
array()
|
||||
);
|
||||
$guests_gauge->set( (int) $guests_total->publish, array() );
|
||||
|
||||
// Guests by status.
|
||||
$guests_status_gauge = $collector->register_gauge(
|
||||
'wp_bnb_guests_by_status',
|
||||
'Number of guests by status',
|
||||
array( 'status' )
|
||||
);
|
||||
|
||||
$guest_statuses = array( 'active', 'blocked', 'vip' );
|
||||
foreach ( $guest_statuses as $status ) {
|
||||
$count = self::count_guests_by_status( $status );
|
||||
$guests_status_gauge->set( $count, array( $status ) );
|
||||
}
|
||||
|
||||
// Repeat guests (guests with more than one booking).
|
||||
$repeat_gauge = $collector->register_gauge(
|
||||
'wp_bnb_guests_repeat',
|
||||
'Number of guests with more than one booking',
|
||||
array()
|
||||
);
|
||||
$repeat_gauge->set( self::count_repeat_guests(), array() );
|
||||
|
||||
// New guests this month.
|
||||
$new_guests_gauge = $collector->register_gauge(
|
||||
'wp_bnb_guests_new_this_month',
|
||||
'Number of new guests registered this month',
|
||||
array()
|
||||
);
|
||||
$new_guests_gauge->set( self::count_new_guests_this_month(), array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect occupancy metrics.
|
||||
*
|
||||
* @param object $collector The wp-prometheus collector instance.
|
||||
* @return void
|
||||
*/
|
||||
private static function collect_occupancy_metrics( $collector ): void {
|
||||
// Current occupancy rate (percentage).
|
||||
$occupancy_gauge = $collector->register_gauge(
|
||||
'wp_bnb_occupancy_rate_current',
|
||||
'Current room occupancy rate (percentage)',
|
||||
array()
|
||||
);
|
||||
$occupancy_gauge->set( self::get_current_occupancy_rate(), array() );
|
||||
|
||||
// Occupancy rate this month.
|
||||
$occupancy_month_gauge = $collector->register_gauge(
|
||||
'wp_bnb_occupancy_rate_this_month',
|
||||
'Room occupancy rate for the current month (percentage)',
|
||||
array()
|
||||
);
|
||||
$occupancy_month_gauge->set( self::get_monthly_occupancy_rate(), array() );
|
||||
|
||||
// Rooms currently occupied.
|
||||
$occupied_gauge = $collector->register_gauge(
|
||||
'wp_bnb_rooms_currently_occupied',
|
||||
'Number of rooms currently occupied',
|
||||
array()
|
||||
);
|
||||
$occupied_gauge->set( self::count_currently_occupied_rooms(), array() );
|
||||
|
||||
// Total room capacity (beds).
|
||||
$capacity_gauge = $collector->register_gauge(
|
||||
'wp_bnb_total_capacity_beds',
|
||||
'Total bed capacity across all rooms',
|
||||
array()
|
||||
);
|
||||
$capacity_gauge->set( self::get_total_bed_capacity(), array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect revenue metrics.
|
||||
*
|
||||
* @param object $collector The wp-prometheus collector instance.
|
||||
* @return void
|
||||
*/
|
||||
private static function collect_revenue_metrics( $collector ): void {
|
||||
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||
|
||||
// Revenue this month.
|
||||
$revenue_month_gauge = $collector->register_gauge(
|
||||
'wp_bnb_revenue_this_month',
|
||||
'Total revenue for the current month',
|
||||
array( 'currency' )
|
||||
);
|
||||
$revenue_month_gauge->set( self::get_revenue_this_month(), array( $currency ) );
|
||||
|
||||
// Revenue year to date.
|
||||
$revenue_ytd_gauge = $collector->register_gauge(
|
||||
'wp_bnb_revenue_ytd',
|
||||
'Total revenue year to date',
|
||||
array( 'currency' )
|
||||
);
|
||||
$revenue_ytd_gauge->set( self::get_revenue_ytd(), array( $currency ) );
|
||||
|
||||
// Average booking value.
|
||||
$avg_value_gauge = $collector->register_gauge(
|
||||
'wp_bnb_booking_avg_value',
|
||||
'Average booking value',
|
||||
array( 'currency' )
|
||||
);
|
||||
$avg_value_gauge->set( self::get_average_booking_value(), array( $currency ) );
|
||||
|
||||
// Revenue from services this month.
|
||||
$services_revenue_gauge = $collector->register_gauge(
|
||||
'wp_bnb_services_revenue_this_month',
|
||||
'Revenue from additional services this month',
|
||||
array( 'currency' )
|
||||
);
|
||||
$services_revenue_gauge->set( self::get_services_revenue_this_month(), array( $currency ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Grafana dashboards.
|
||||
*
|
||||
* @param object $provider The wp-prometheus dashboard provider instance.
|
||||
* @return void
|
||||
*/
|
||||
public static function register_dashboards( $provider ): void {
|
||||
$dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json';
|
||||
|
||||
if ( file_exists( $dashboard_file ) ) {
|
||||
$provider->register_dashboard(
|
||||
'wp-bnb',
|
||||
array(
|
||||
'title' => __( 'WP BnB Dashboard', 'wp-bnb' ),
|
||||
'description' => __( 'Monitor occupancy, bookings, revenue, and guest statistics for your B&B.', 'wp-bnb' ),
|
||||
'icon' => 'dashicons-building',
|
||||
'file' => $dashboard_file,
|
||||
'plugin' => 'WP BnB Manager',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods - Inventory
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Count rooms by status.
|
||||
*
|
||||
* @param string $status Room status.
|
||||
* @return int
|
||||
*/
|
||||
private static function count_rooms_by_status( string $status ): int {
|
||||
global $wpdb;
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm.meta_key = '_bnb_room_status'
|
||||
AND pm.meta_value = %s",
|
||||
Room::POST_TYPE,
|
||||
$status
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count services by status.
|
||||
*
|
||||
* @param string $status Service status.
|
||||
* @return int
|
||||
*/
|
||||
private static function count_services_by_status( string $status ): int {
|
||||
global $wpdb;
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm.meta_key = '_bnb_service_status'
|
||||
AND pm.meta_value = %s",
|
||||
Service::POST_TYPE,
|
||||
$status
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods - Bookings
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Count bookings by status.
|
||||
*
|
||||
* @param string $status Booking status.
|
||||
* @return int
|
||||
*/
|
||||
private static function count_bookings_by_status( string $status ): int {
|
||||
global $wpdb;
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm.meta_key = '_bnb_booking_status'
|
||||
AND pm.meta_value = %s",
|
||||
Booking::POST_TYPE,
|
||||
$status
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count today's check-ins.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function count_todays_checkins(): int {
|
||||
global $wpdb;
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_date.meta_key = '_bnb_booking_check_in'
|
||||
AND pm_date.meta_value = %s
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value IN ('confirmed', 'pending')",
|
||||
Booking::POST_TYPE,
|
||||
$today
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count today's check-outs.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function count_todays_checkouts(): int {
|
||||
global $wpdb;
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_date.meta_key = '_bnb_booking_check_out'
|
||||
AND pm_date.meta_value = %s
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value = 'checked_in'",
|
||||
Booking::POST_TYPE,
|
||||
$today
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count upcoming bookings within given days.
|
||||
*
|
||||
* @param int $days Number of days to look ahead.
|
||||
* @return int
|
||||
*/
|
||||
private static function count_upcoming_bookings( int $days ): int {
|
||||
global $wpdb;
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
$end_date = gmdate( 'Y-m-d', strtotime( "+{$days} days" ) );
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_date.meta_key = '_bnb_booking_check_in'
|
||||
AND pm_date.meta_value >= %s
|
||||
AND pm_date.meta_value <= %s
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value IN ('confirmed', 'pending')",
|
||||
Booking::POST_TYPE,
|
||||
$today,
|
||||
$end_date
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get average booking duration in nights.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function get_average_booking_duration(): float {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT AVG(DATEDIFF(pm_out.meta_value, pm_in.meta_value))
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value NOT IN ('cancelled')",
|
||||
Booking::POST_TYPE
|
||||
)
|
||||
);
|
||||
|
||||
return round( (float) $result, 1 );
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods - Guests
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Count guests by status.
|
||||
*
|
||||
* @param string $status Guest status.
|
||||
* @return int
|
||||
*/
|
||||
private static function count_guests_by_status( string $status ): int {
|
||||
global $wpdb;
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm.meta_key = '_bnb_guest_status'
|
||||
AND pm.meta_value = %s",
|
||||
Guest::POST_TYPE,
|
||||
$status
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count repeat guests (more than one booking).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function count_repeat_guests(): int {
|
||||
global $wpdb;
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT pm.meta_value)
|
||||
FROM {$wpdb->postmeta} pm
|
||||
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm.meta_key = '_bnb_booking_guest_id'
|
||||
AND pm.meta_value != ''
|
||||
GROUP BY pm.meta_value
|
||||
HAVING COUNT(*) > 1",
|
||||
Booking::POST_TYPE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count new guests registered this month.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function count_new_guests_this_month(): int {
|
||||
global $wpdb;
|
||||
$first_of_month = gmdate( 'Y-m-01 00:00:00' );
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM {$wpdb->posts}
|
||||
WHERE post_type = %s
|
||||
AND post_status = 'publish'
|
||||
AND post_date >= %s",
|
||||
Guest::POST_TYPE,
|
||||
$first_of_month
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods - Occupancy
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get current occupancy rate (percentage of rooms occupied today).
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function get_current_occupancy_rate(): float {
|
||||
$total_rooms = self::count_available_rooms();
|
||||
if ( $total_rooms <= 0 ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$occupied = self::count_currently_occupied_rooms();
|
||||
return round( ( $occupied / $total_rooms ) * 100, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monthly occupancy rate.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function get_monthly_occupancy_rate(): float {
|
||||
global $wpdb;
|
||||
|
||||
$total_rooms = self::count_available_rooms();
|
||||
if ( $total_rooms <= 0 ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$first_of_month = gmdate( 'Y-m-01' );
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
$days_so_far = (int) gmdate( 'd' );
|
||||
$total_room_nights = $total_rooms * $days_so_far;
|
||||
|
||||
// Count booked nights this month (simplified: count bookings that overlap with this month).
|
||||
$booked_nights = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(
|
||||
DATEDIFF(
|
||||
LEAST(pm_out.meta_value, %s),
|
||||
GREATEST(pm_in.meta_value, %s)
|
||||
)
|
||||
)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')
|
||||
AND pm_in.meta_value <= %s
|
||||
AND pm_out.meta_value >= %s",
|
||||
$today,
|
||||
$first_of_month,
|
||||
Booking::POST_TYPE,
|
||||
$today,
|
||||
$first_of_month
|
||||
)
|
||||
);
|
||||
|
||||
if ( $total_room_nights <= 0 ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return round( ( $booked_nights / $total_room_nights ) * 100, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Count currently occupied rooms.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function count_currently_occupied_rooms(): int {
|
||||
global $wpdb;
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT pm_room.meta_value)
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_room ON p.ID = pm_room.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_room.meta_key = '_bnb_booking_room_id'
|
||||
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value IN ('confirmed', 'checked_in')
|
||||
AND pm_in.meta_value <= %s
|
||||
AND pm_out.meta_value > %s",
|
||||
Booking::POST_TYPE,
|
||||
$today,
|
||||
$today
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count available rooms (not in maintenance/inactive).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function count_available_rooms(): int {
|
||||
global $wpdb;
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT p.ID)
|
||||
FROM {$wpdb->posts} p
|
||||
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_bnb_room_status'
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND (pm.meta_value IS NULL OR pm.meta_value IN ('available', 'occupied'))",
|
||||
Room::POST_TYPE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total bed capacity.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function get_total_bed_capacity(): int {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(CAST(pm.meta_value AS UNSIGNED))
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm.meta_key = '_bnb_room_beds'",
|
||||
Room::POST_TYPE
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $result;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods - Revenue
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get revenue for the current month.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function get_revenue_this_month(): float {
|
||||
global $wpdb;
|
||||
$first_of_month = gmdate( 'Y-m-01' );
|
||||
|
||||
$result = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||
AND pm_in.meta_value >= %s
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||
Booking::POST_TYPE,
|
||||
$first_of_month
|
||||
)
|
||||
);
|
||||
|
||||
return round( (float) $result, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get revenue year to date.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function get_revenue_ytd(): float {
|
||||
global $wpdb;
|
||||
$first_of_year = gmdate( 'Y-01-01' );
|
||||
|
||||
$result = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||
AND pm_in.meta_value >= %s
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||
Booking::POST_TYPE,
|
||||
$first_of_year
|
||||
)
|
||||
);
|
||||
|
||||
return round( (float) $result, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get average booking value.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function get_average_booking_value(): float {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT AVG(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||
Booking::POST_TYPE
|
||||
)
|
||||
);
|
||||
|
||||
return round( (float) $result, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get revenue from services this month.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function get_services_revenue_this_month(): float {
|
||||
global $wpdb;
|
||||
$first_of_month = gmdate( 'Y-m-01' );
|
||||
|
||||
$result = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(CAST(pm_services.meta_value AS DECIMAL(10,2)))
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm_services ON p.ID = pm_services.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||
WHERE p.post_type = %s
|
||||
AND p.post_status = 'publish'
|
||||
AND pm_services.meta_key = '_bnb_booking_services_total'
|
||||
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||
AND pm_in.meta_value >= %s
|
||||
AND pm_status.meta_key = '_bnb_booking_status'
|
||||
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||
Booking::POST_TYPE,
|
||||
$first_of_month
|
||||
)
|
||||
);
|
||||
|
||||
return round( (float) $result, 2 );
|
||||
}
|
||||
}
|
||||
184
src/Plugin.php
184
src/Plugin.php
@@ -19,6 +19,7 @@ use Magdev\WpBnb\Booking\EmailNotifier;
|
||||
use Magdev\WpBnb\Frontend\Search;
|
||||
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||
use Magdev\WpBnb\Integration\CF7;
|
||||
use Magdev\WpBnb\Integration\Prometheus;
|
||||
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
||||
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
||||
@@ -142,6 +143,9 @@ final class Plugin {
|
||||
CF7::init();
|
||||
}
|
||||
|
||||
// Initialize Prometheus metrics integration.
|
||||
Prometheus::init();
|
||||
|
||||
// Initialize admin components.
|
||||
if ( is_admin() ) {
|
||||
$this->init_admin();
|
||||
@@ -610,6 +614,10 @@ final class Plugin {
|
||||
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=metrics' ) ); ?>"
|
||||
class="nav-tab <?php echo 'metrics' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||
<?php esc_html_e( 'Metrics', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -624,6 +632,9 @@ final class Plugin {
|
||||
case 'updates':
|
||||
$this->render_updates_settings();
|
||||
break;
|
||||
case 'metrics':
|
||||
$this->render_metrics_settings();
|
||||
break;
|
||||
default:
|
||||
$this->render_general_settings();
|
||||
break;
|
||||
@@ -1265,6 +1276,163 @@ final class Plugin {
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render metrics settings tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_metrics_settings(): void {
|
||||
$metrics_enabled = Prometheus::is_enabled();
|
||||
$prometheus_active = class_exists( '\Magdev\WpPrometheus\Plugin' ) || defined( 'WP_PROMETHEUS_VERSION' );
|
||||
$dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json';
|
||||
$dashboard_available = file_exists( $dashboard_file );
|
||||
?>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||
|
||||
<h2><?php esc_html_e( 'Prometheus Metrics', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<?php if ( ! $prometheus_active ) : ?>
|
||||
<div class="notice notice-warning inline" style="margin: 0 0 20px 0;">
|
||||
<p>
|
||||
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
|
||||
<strong><?php esc_html_e( 'WP Prometheus not detected', 'wp-bnb' ); ?></strong><br>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Plugin URL */
|
||||
esc_html__( 'The WP Prometheus plugin is required to expose metrics. Please install and activate it from %s.', 'wp-bnb' ),
|
||||
'<a href="https://src.bundespruefstelle.ch/magdev/wp-prometheus" target="_blank">wp-prometheus</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="notice notice-success inline" style="margin: 0 0 20px 0;">
|
||||
<p>
|
||||
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
|
||||
<strong><?php esc_html_e( 'WP Prometheus is active', 'wp-bnb' ); ?></strong>
|
||||
<?php esc_html_e( 'Metrics will be exposed via the /metrics/ endpoint.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Enable Metrics', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="wp_bnb_metrics_enabled"
|
||||
value="yes" <?php checked( $metrics_enabled ); ?>>
|
||||
<?php esc_html_e( 'Expose BnB metrics via Prometheus', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'When enabled, occupancy, booking, revenue, and guest metrics will be available for Prometheus scraping.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Available Metrics', 'wp-bnb' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'The following metrics are exposed when enabled:', 'wp-bnb' ); ?></p>
|
||||
|
||||
<table class="widefat striped" style="max-width: 800px; margin-top: 10px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Metric', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Type', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>wp_bnb_buildings_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Total number of buildings', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_rooms_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Total rooms by status (available, occupied, maintenance, inactive)', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_bookings_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Total bookings by status (pending, confirmed, checked_in, checked_out, cancelled)', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_occupancy_rate_current</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Current room occupancy rate (percentage)', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_occupancy_rate_this_month</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Room occupancy rate for current month (percentage)', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_checkins_today</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Number of check-ins scheduled for today', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_checkouts_today</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Number of check-outs scheduled for today', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_revenue_this_month</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Total revenue for current month', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_revenue_ytd</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Total revenue year to date', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_booking_avg_value</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Average booking value', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_guests_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Total number of registered guests', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wp_bnb_guests_repeat</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||
<td><?php esc_html_e( 'Number of repeat guests (more than one booking)', 'wp-bnb' ); ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Grafana Dashboard', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<?php if ( $dashboard_available ) : ?>
|
||||
<p>
|
||||
<?php esc_html_e( 'A pre-configured Grafana dashboard is available for visualizing WP BnB metrics.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Dashboard file:', 'wp-bnb' ); ?></strong>
|
||||
<code>assets/grafana/wp-bnb-dashboard.json</code>
|
||||
</p>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'If WP Prometheus is installed, this dashboard will be automatically registered and available for export in the Prometheus settings.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
<?php else : ?>
|
||||
<div class="notice notice-warning inline">
|
||||
<p><?php esc_html_e( 'Grafana dashboard file not found.', 'wp-bnb' ); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="submit">
|
||||
<?php submit_button( __( 'Save Metrics Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||
</p>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render license status badge.
|
||||
*
|
||||
@@ -1336,6 +1504,9 @@ final class Plugin {
|
||||
case 'updates':
|
||||
$this->save_updates_settings();
|
||||
break;
|
||||
case 'metrics':
|
||||
$this->save_metrics_settings();
|
||||
break;
|
||||
default:
|
||||
$this->save_general_settings();
|
||||
break;
|
||||
@@ -1465,6 +1636,19 @@ final class Plugin {
|
||||
settings_errors( 'wp_bnb_settings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save metrics settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function save_metrics_settings(): void {
|
||||
$metrics_enabled = isset( $_POST['wp_bnb_metrics_enabled'] ) ? 'yes' : 'no';
|
||||
update_option( Prometheus::OPTION_ENABLED, $metrics_enabled );
|
||||
|
||||
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Metrics settings saved.', 'wp-bnb' ), 'success' );
|
||||
settings_errors( 'wp_bnb_settings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for checking room availability.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user