878 lines
24 KiB
PHP
878 lines
24 KiB
PHP
|
|
<?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 );
|
||
|
|
}
|
||
|
|
}
|