Implement Phase 8: Dashboard & Reports (v0.8.0)
Some checks failed
Create Release Package / build-release (push) Has been cancelled
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:
942
src/Admin/Dashboard.php
Normal file
942
src/Admin/Dashboard.php
Normal 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
1368
src/Admin/Reports.php
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user