Implement Phase 8: Dashboard & Reports (v0.8.0)
Some checks failed
Create Release Package / build-release (push) Has been cancelled

- Add comprehensive admin dashboard with stat cards and widgets
- Add Chart.js for occupancy/revenue trend charts
- Add Reports page with Occupancy, Revenue, Guest tabs
- Add CSV and PDF export functionality (using mPDF)
- Add date range filters for reports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 20:20:27 +01:00
parent 992d961066
commit b137fec4fb
11 changed files with 3739 additions and 87 deletions

942
src/Admin/Dashboard.php Normal file
View File

@@ -0,0 +1,942 @@
<?php
/**
* Admin Dashboard page.
*
* Displays comprehensive dashboard with statistics, charts, and quick actions.
*
* @package Magdev\WpBnb\Admin
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Admin;
use Magdev\WpBnb\Booking\Availability;
use Magdev\WpBnb\License\Manager as LicenseManager;
use Magdev\WpBnb\PostTypes\Booking;
use Magdev\WpBnb\PostTypes\Building;
use Magdev\WpBnb\PostTypes\Guest;
use Magdev\WpBnb\PostTypes\Room;
use Magdev\WpBnb\Pricing\Calculator;
/**
* Dashboard class.
*/
final class Dashboard {
/**
* Cache key for dashboard stats.
*/
private const CACHE_KEY = 'wp_bnb_dashboard_stats';
/**
* Cache expiry in seconds (1 hour).
*/
private const CACHE_EXPIRY = HOUR_IN_SECONDS;
/**
* Render the dashboard page.
*
* @return void
*/
public static function render(): void {
$license_valid = LicenseManager::is_license_valid();
$is_localhost = LicenseManager::is_localhost();
?>
<div class="wrap">
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
<?php self::render_notices( $license_valid, $is_localhost ); ?>
<div class="wp-bnb-dashboard-grid">
<!-- Row 1: Stats Cards -->
<div class="wp-bnb-dashboard-row wp-bnb-stats-row">
<?php self::render_occupancy_card(); ?>
<?php self::render_revenue_card(); ?>
<?php self::render_bookings_card(); ?>
<?php self::render_guests_card(); ?>
</div>
<!-- Row 2: Charts -->
<div class="wp-bnb-dashboard-row wp-bnb-charts-row">
<?php self::render_occupancy_chart(); ?>
<?php self::render_revenue_chart(); ?>
</div>
<!-- Row 3: Activity and Quick Actions -->
<div class="wp-bnb-dashboard-row wp-bnb-activity-row">
<?php self::render_today_activity(); ?>
<?php self::render_upcoming_bookings(); ?>
<?php self::render_quick_actions(); ?>
</div>
</div>
</div>
<?php
}
/**
* Render admin notices.
*
* @param bool $license_valid Whether license is valid.
* @param bool $is_localhost Whether running on localhost.
* @return void
*/
private static function render_notices( bool $license_valid, bool $is_localhost ): void {
if ( $is_localhost ) :
?>
<div class="notice notice-info">
<p>
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
<strong><?php esc_html_e( 'Development Mode', 'wp-bnb' ); ?></strong>
<?php esc_html_e( 'You are running on a local development environment. All features are enabled.', 'wp-bnb' ); ?>
</p>
</div>
<?php
elseif ( ! $license_valid ) :
?>
<div class="notice notice-warning">
<p>
<?php
printf(
/* translators: %s: Link to settings page */
esc_html__( 'Your license is not active. Please %s to unlock all features.', 'wp-bnb' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=license' ) ) . '">' . esc_html__( 'activate your license', 'wp-bnb' ) . '</a>'
);
?>
</p>
</div>
<?php
endif;
}
/**
* Render occupancy stat card.
*
* @return void
*/
private static function render_occupancy_card(): void {
$stats = self::get_occupancy_stats();
$rate = $stats['rate'];
$occupied = $stats['occupied'];
$total = $stats['total'];
$previous_rate = $stats['previous_rate'];
$change = $rate - $previous_rate;
$change_class = $change >= 0 ? 'positive' : 'negative';
$change_icon = $change >= 0 ? 'arrow-up-alt' : 'arrow-down-alt';
?>
<div class="wp-bnb-stat-card">
<div class="wp-bnb-stat-icon">
<span class="dashicons dashicons-admin-home"></span>
</div>
<div class="wp-bnb-stat-content">
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Current Occupancy', 'wp-bnb' ); ?></div>
<div class="wp-bnb-stat-value"><?php echo esc_html( number_format( $rate, 1 ) ); ?>%</div>
<div class="wp-bnb-stat-meta">
<?php
printf(
/* translators: 1: Number of occupied rooms, 2: Total rooms */
esc_html__( '%1$d of %2$d rooms', 'wp-bnb' ),
$occupied,
$total
);
?>
</div>
<?php if ( $previous_rate > 0 ) : ?>
<div class="wp-bnb-stat-change <?php echo esc_attr( $change_class ); ?>">
<span class="dashicons dashicons-<?php echo esc_attr( $change_icon ); ?>"></span>
<?php
printf(
/* translators: %s: Percentage change */
esc_html__( '%s%% vs last month', 'wp-bnb' ),
number_format( abs( $change ), 1 )
);
?>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Render revenue stat card.
*
* @return void
*/
private static function render_revenue_card(): void {
$stats = self::get_revenue_stats();
$this_month = $stats['this_month'];
$last_month = $stats['last_month'];
$change = $last_month > 0 ? ( ( $this_month - $last_month ) / $last_month ) * 100 : 0;
$change_class = $change >= 0 ? 'positive' : 'negative';
$change_icon = $change >= 0 ? 'arrow-up-alt' : 'arrow-down-alt';
?>
<div class="wp-bnb-stat-card">
<div class="wp-bnb-stat-icon revenue">
<span class="dashicons dashicons-chart-area"></span>
</div>
<div class="wp-bnb-stat-content">
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Revenue This Month', 'wp-bnb' ); ?></div>
<div class="wp-bnb-stat-value"><?php echo esc_html( Calculator::formatPrice( $this_month ) ); ?></div>
<div class="wp-bnb-stat-meta">
<?php
printf(
/* translators: %s: Year-to-date revenue */
esc_html__( 'YTD: %s', 'wp-bnb' ),
Calculator::formatPrice( $stats['ytd'] )
);
?>
</div>
<?php if ( $last_month > 0 ) : ?>
<div class="wp-bnb-stat-change <?php echo esc_attr( $change_class ); ?>">
<span class="dashicons dashicons-<?php echo esc_attr( $change_icon ); ?>"></span>
<?php
printf(
/* translators: %s: Percentage change */
esc_html__( '%s%% vs last month', 'wp-bnb' ),
number_format( abs( $change ), 1 )
);
?>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Render bookings stat card.
*
* @return void
*/
private static function render_bookings_card(): void {
$stats = self::get_booking_stats();
?>
<div class="wp-bnb-stat-card">
<div class="wp-bnb-stat-icon bookings">
<span class="dashicons dashicons-calendar-alt"></span>
</div>
<div class="wp-bnb-stat-content">
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Bookings This Month', 'wp-bnb' ); ?></div>
<div class="wp-bnb-stat-value"><?php echo esc_html( $stats['this_month'] ); ?></div>
<div class="wp-bnb-stat-meta">
<?php
printf(
/* translators: 1: Pending count, 2: Confirmed count */
esc_html__( '%1$d pending, %2$d confirmed', 'wp-bnb' ),
$stats['pending'],
$stats['confirmed']
);
?>
</div>
</div>
</div>
<?php
}
/**
* Render guests stat card.
*
* @return void
*/
private static function render_guests_card(): void {
$stats = self::get_guest_stats();
?>
<div class="wp-bnb-stat-card">
<div class="wp-bnb-stat-icon guests">
<span class="dashicons dashicons-groups"></span>
</div>
<div class="wp-bnb-stat-content">
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Total Guests', 'wp-bnb' ); ?></div>
<div class="wp-bnb-stat-value"><?php echo esc_html( $stats['total'] ); ?></div>
<div class="wp-bnb-stat-meta">
<?php
printf(
/* translators: 1: New guests this month, 2: Repeat guests count */
esc_html__( '%1$d new this month, %2$d repeat', 'wp-bnb' ),
$stats['new_this_month'],
$stats['repeat']
);
?>
</div>
</div>
</div>
<?php
}
/**
* Render occupancy trend chart.
*
* @return void
*/
private static function render_occupancy_chart(): void {
?>
<div class="wp-bnb-widget wp-bnb-chart-widget">
<div class="wp-bnb-widget-header">
<h3><?php esc_html_e( 'Occupancy Trend (Last 30 Days)', 'wp-bnb' ); ?></h3>
</div>
<div class="wp-bnb-widget-content">
<canvas id="wp-bnb-occupancy-chart" height="200"></canvas>
</div>
</div>
<?php
}
/**
* Render revenue trend chart.
*
* @return void
*/
private static function render_revenue_chart(): void {
?>
<div class="wp-bnb-widget wp-bnb-chart-widget">
<div class="wp-bnb-widget-header">
<h3><?php esc_html_e( 'Revenue Trend (Last 6 Months)', 'wp-bnb' ); ?></h3>
</div>
<div class="wp-bnb-widget-content">
<canvas id="wp-bnb-revenue-chart" height="200"></canvas>
</div>
</div>
<?php
}
/**
* Render today's activity widget.
*
* @return void
*/
private static function render_today_activity(): void {
$checkins = Availability::get_todays_checkins();
$checkouts = Availability::get_todays_checkouts();
?>
<div class="wp-bnb-widget">
<div class="wp-bnb-widget-header">
<h3><?php esc_html_e( "Today's Activity", 'wp-bnb' ); ?></h3>
<span class="wp-bnb-widget-date"><?php echo esc_html( wp_date( get_option( 'date_format' ) ) ); ?></span>
</div>
<div class="wp-bnb-widget-content">
<?php if ( empty( $checkins ) && empty( $checkouts ) ) : ?>
<div class="wp-bnb-empty-state">
<span class="dashicons dashicons-calendar"></span>
<p><?php esc_html_e( 'No check-ins or check-outs scheduled for today.', 'wp-bnb' ); ?></p>
</div>
<?php else : ?>
<?php if ( ! empty( $checkins ) ) : ?>
<div class="wp-bnb-activity-section">
<h4>
<span class="dashicons dashicons-migrate"></span>
<?php esc_html_e( 'Check-ins', 'wp-bnb' ); ?>
<span class="count"><?php echo count( $checkins ); ?></span>
</h4>
<ul class="wp-bnb-activity-list">
<?php foreach ( $checkins as $booking ) : ?>
<?php
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
$room = get_post( $room_id );
?>
<li>
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
<strong><?php echo esc_html( $guest_name ); ?></strong>
<?php if ( $room ) : ?>
<span class="room"><?php echo esc_html( $room->post_title ); ?></span>
<?php endif; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ( ! empty( $checkouts ) ) : ?>
<div class="wp-bnb-activity-section">
<h4>
<span class="dashicons dashicons-external"></span>
<?php esc_html_e( 'Check-outs', 'wp-bnb' ); ?>
<span class="count"><?php echo count( $checkouts ); ?></span>
</h4>
<ul class="wp-bnb-activity-list">
<?php foreach ( $checkouts as $booking ) : ?>
<?php
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
$room = get_post( $room_id );
?>
<li>
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
<strong><?php echo esc_html( $guest_name ); ?></strong>
<?php if ( $room ) : ?>
<span class="room"><?php echo esc_html( $room->post_title ); ?></span>
<?php endif; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Render upcoming bookings widget.
*
* @return void
*/
private static function render_upcoming_bookings(): void {
$bookings = self::get_upcoming_bookings( 7 );
?>
<div class="wp-bnb-widget">
<div class="wp-bnb-widget-header">
<h3><?php esc_html_e( 'Upcoming Bookings', 'wp-bnb' ); ?></h3>
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=' . Booking::POST_TYPE ) ); ?>" class="wp-bnb-view-all">
<?php esc_html_e( 'View All', 'wp-bnb' ); ?>
</a>
</div>
<div class="wp-bnb-widget-content">
<?php if ( empty( $bookings ) ) : ?>
<div class="wp-bnb-empty-state">
<span class="dashicons dashicons-calendar-alt"></span>
<p><?php esc_html_e( 'No upcoming bookings in the next 7 days.', 'wp-bnb' ); ?></p>
</div>
<?php else : ?>
<table class="wp-bnb-upcoming-table">
<thead>
<tr>
<th><?php esc_html_e( 'Guest', 'wp-bnb' ); ?></th>
<th><?php esc_html_e( 'Room', 'wp-bnb' ); ?></th>
<th><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></th>
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $bookings as $booking ) : ?>
<?php
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
$room = get_post( $room_id );
$statuses = Booking::get_booking_statuses();
$colors = Booking::get_status_colors();
?>
<tr>
<td>
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
<?php echo esc_html( $guest_name ); ?>
</a>
</td>
<td><?php echo $room ? esc_html( $room->post_title ) : '—'; ?></td>
<td><?php echo esc_html( wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) ); ?></td>
<td>
<span class="wp-bnb-status-badge" style="background-color: <?php echo esc_attr( $colors[ $status ] ?? '#666' ); ?>">
<?php echo esc_html( $statuses[ $status ] ?? $status ); ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Render quick actions widget.
*
* @return void
*/
private static function render_quick_actions(): void {
?>
<div class="wp-bnb-widget wp-bnb-quick-actions">
<div class="wp-bnb-widget-header">
<h3><?php esc_html_e( 'Quick Actions', 'wp-bnb' ); ?></h3>
</div>
<div class="wp-bnb-widget-content">
<div class="wp-bnb-actions-grid">
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Booking::POST_TYPE ) ); ?>" class="wp-bnb-action-btn">
<span class="dashicons dashicons-plus-alt"></span>
<span><?php esc_html_e( 'New Booking', 'wp-bnb' ); ?></span>
</a>
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Guest::POST_TYPE ) ); ?>" class="wp-bnb-action-btn">
<span class="dashicons dashicons-admin-users"></span>
<span><?php esc_html_e( 'New Guest', 'wp-bnb' ); ?></span>
</a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-calendar' ) ); ?>" class="wp-bnb-action-btn">
<span class="dashicons dashicons-calendar-alt"></span>
<span><?php esc_html_e( 'View Calendar', 'wp-bnb' ); ?></span>
</a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-reports' ) ); ?>" class="wp-bnb-action-btn">
<span class="dashicons dashicons-analytics"></span>
<span><?php esc_html_e( 'View Reports', 'wp-bnb' ); ?></span>
</a>
</div>
</div>
</div>
<?php
}
/**
* Get occupancy statistics.
*
* @return array{rate: float, occupied: int, total: int, previous_rate: float}
*/
public static function get_occupancy_stats(): array {
// Get total rooms.
$rooms = get_posts(
array(
'post_type' => Room::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
)
);
$total_rooms = count( $rooms );
// Get currently occupied rooms.
$current_bookings = Availability::get_current_bookings();
$occupied_rooms = count( $current_bookings );
$rate = $total_rooms > 0 ? ( $occupied_rooms / $total_rooms ) * 100 : 0;
// Calculate last month's average occupancy.
$previous_rate = self::get_average_occupancy_for_month(
(int) gmdate( 'Y', strtotime( '-1 month' ) ),
(int) gmdate( 'n', strtotime( '-1 month' ) )
);
return array(
'rate' => $rate,
'occupied' => $occupied_rooms,
'total' => $total_rooms,
'previous_rate' => $previous_rate,
);
}
/**
* Get average occupancy rate for a specific month.
*
* @param int $year Year.
* @param int $month Month.
* @return float Average occupancy percentage.
*/
private static function get_average_occupancy_for_month( int $year, int $month ): float {
$cache_key = self::CACHE_KEY . "_occupancy_{$year}_{$month}";
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return (float) $cached;
}
$rooms = get_posts(
array(
'post_type' => Room::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
)
);
$total_rooms = count( $rooms );
if ( $total_rooms === 0 ) {
return 0.0;
}
$days_in_month = (int) gmdate( 't', mktime( 0, 0, 0, $month, 1, $year ) );
$total_room_nights = $total_rooms * $days_in_month;
$booked_nights = 0;
$month_start = sprintf( '%04d-%02d-01', $year, $month );
$month_end = sprintf( '%04d-%02d-%02d', $year, $month, $days_in_month );
$bookings = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_bnb_booking_status',
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
'compare' => 'IN',
),
array(
'key' => '_bnb_booking_check_in',
'value' => $month_end,
'compare' => '<=',
'type' => 'DATE',
),
array(
'key' => '_bnb_booking_check_out',
'value' => $month_start,
'compare' => '>=',
'type' => 'DATE',
),
),
)
);
foreach ( $bookings as $booking ) {
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
// Clamp to month boundaries.
$start = max( $check_in, $month_start );
$end = min( $check_out, gmdate( 'Y-m-d', strtotime( $month_end . ' +1 day' ) ) );
$nights = Booking::calculate_nights( $start, $end );
$booked_nights += $nights;
}
$rate = ( $booked_nights / $total_room_nights ) * 100;
set_transient( $cache_key, $rate, self::CACHE_EXPIRY );
return $rate;
}
/**
* Get revenue statistics.
*
* @return array{this_month: float, last_month: float, ytd: float}
*/
public static function get_revenue_stats(): array {
$this_month = self::get_revenue_for_period(
gmdate( 'Y-m-01' ),
gmdate( 'Y-m-t' )
);
$last_month = self::get_revenue_for_period(
gmdate( 'Y-m-01', strtotime( '-1 month' ) ),
gmdate( 'Y-m-t', strtotime( '-1 month' ) )
);
$ytd = self::get_revenue_for_period(
gmdate( 'Y-01-01' ),
gmdate( 'Y-m-d' )
);
return array(
'this_month' => $this_month,
'last_month' => $last_month,
'ytd' => $ytd,
);
}
/**
* Get revenue for a specific period.
*
* @param string $start_date Start date (Y-m-d).
* @param string $end_date End date (Y-m-d).
* @return float Total revenue.
*/
public static function get_revenue_for_period( string $start_date, string $end_date ): float {
$bookings = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_bnb_booking_status',
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
'compare' => 'IN',
),
array(
'key' => '_bnb_booking_check_in',
'value' => $start_date,
'compare' => '>=',
'type' => 'DATE',
),
array(
'key' => '_bnb_booking_check_in',
'value' => $end_date,
'compare' => '<=',
'type' => 'DATE',
),
),
)
);
$total = 0.0;
foreach ( $bookings as $booking ) {
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
$total += (float) $price;
// Add services total.
$services_total = Booking::calculate_booking_services_total( $booking->ID );
$total += $services_total;
}
return $total;
}
/**
* Get booking statistics.
*
* @return array{this_month: int, pending: int, confirmed: int}
*/
public static function get_booking_stats(): array {
$month_start = gmdate( 'Y-m-01' );
$month_end = gmdate( 'Y-m-t' );
// Bookings created this month.
$this_month = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'date_query' => array(
array(
'after' => $month_start,
'before' => $month_end,
'inclusive' => true,
),
),
)
);
// Pending bookings.
$pending = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
array(
'key' => '_bnb_booking_status',
'value' => 'pending',
),
),
)
);
// Confirmed bookings.
$confirmed = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
array(
'key' => '_bnb_booking_status',
'value' => 'confirmed',
),
),
)
);
return array(
'this_month' => count( $this_month ),
'pending' => count( $pending ),
'confirmed' => count( $confirmed ),
);
}
/**
* Get guest statistics.
*
* @return array{total: int, new_this_month: int, repeat: int}
*/
public static function get_guest_stats(): array {
$month_start = gmdate( 'Y-m-01' );
$month_end = gmdate( 'Y-m-t' );
// Total guests.
$total = get_posts(
array(
'post_type' => Guest::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
)
);
// New guests this month.
$new_this_month = get_posts(
array(
'post_type' => Guest::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'date_query' => array(
array(
'after' => $month_start,
'before' => $month_end,
'inclusive' => true,
),
),
)
);
// Repeat guests (2+ bookings).
$all_guests = get_posts(
array(
'post_type' => Guest::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
)
);
$repeat = 0;
foreach ( $all_guests as $guest ) {
$booking_count = Guest::get_booking_count( $guest->ID );
if ( $booking_count >= 2 ) {
++$repeat;
}
}
return array(
'total' => count( $total ),
'new_this_month' => count( $new_this_month ),
'repeat' => $repeat,
);
}
/**
* Get upcoming bookings.
*
* @param int $days Number of days to look ahead.
* @return array<\WP_Post> Array of booking posts.
*/
private static function get_upcoming_bookings( int $days = 7 ): array {
$today = gmdate( 'Y-m-d' );
$end_date = gmdate( 'Y-m-d', strtotime( "+{$days} days" ) );
return get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => 10,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_bnb_booking_status',
'value' => array( 'pending', 'confirmed' ),
'compare' => 'IN',
),
array(
'key' => '_bnb_booking_check_in',
'value' => $today,
'compare' => '>=',
'type' => 'DATE',
),
array(
'key' => '_bnb_booking_check_in',
'value' => $end_date,
'compare' => '<=',
'type' => 'DATE',
),
),
'orderby' => 'meta_value',
'meta_key' => '_bnb_booking_check_in',
'order' => 'ASC',
)
);
}
/**
* Get occupancy trend data for charts.
*
* @param int $days Number of days to include.
* @return array{labels: array<string>, data: array<float>}
*/
public static function get_occupancy_trend_data( int $days = 30 ): array {
$labels = array();
$data = array();
$rooms = get_posts(
array(
'post_type' => Room::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
)
);
$total_rooms = count( $rooms );
if ( $total_rooms === 0 ) {
return array(
'labels' => array(),
'data' => array(),
);
}
for ( $i = $days - 1; $i >= 0; $i-- ) {
$date = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
$labels[] = wp_date( 'M j', strtotime( $date ) );
// Count bookings active on this date.
$bookings = get_posts(
array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_bnb_booking_status',
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
'compare' => 'IN',
),
array(
'key' => '_bnb_booking_check_in',
'value' => $date,
'compare' => '<=',
'type' => 'DATE',
),
array(
'key' => '_bnb_booking_check_out',
'value' => $date,
'compare' => '>',
'type' => 'DATE',
),
),
)
);
$rate = ( count( $bookings ) / $total_rooms ) * 100;
$data[] = round( $rate, 1 );
}
return array(
'labels' => $labels,
'data' => $data,
);
}
/**
* Get revenue trend data for charts.
*
* @param int $months Number of months to include.
* @return array{labels: array<string>, data: array<float>}
*/
public static function get_revenue_trend_data( int $months = 6 ): array {
$labels = array();
$data = array();
for ( $i = $months - 1; $i >= 0; $i-- ) {
$month_start = gmdate( 'Y-m-01', strtotime( "-{$i} months" ) );
$month_end = gmdate( 'Y-m-t', strtotime( "-{$i} months" ) );
$month_name = gmdate( 'M Y', strtotime( $month_start ) );
$labels[] = $month_name;
$data[] = self::get_revenue_for_period( $month_start, $month_end );
}
return array(
'labels' => $labels,
'data' => $data,
);
}
}

1368
src/Admin/Reports.php Normal file

File diff suppressed because it is too large Load Diff