Implement Phase 9: Prometheus Metrics (v0.9.0)
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:
2026-02-03 20:40:54 +01:00
parent b6d7eeb5ec
commit 13fd25f84c
8 changed files with 2805 additions and 8 deletions

View 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 );
}
}

View File

@@ -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.
*