Files
wp-bnb/src/Admin/Reports.php

1369 lines
50 KiB
PHP
Raw Normal View History

<?php
/**
* Admin Reports page.
*
* Displays reports with occupancy, revenue, guest statistics, and export options.
*
* @package Magdev\WpBnb\Admin
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Admin;
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;
use Mpdf\Mpdf;
/**
* Reports class.
*/
final class Reports {
/**
* Render the reports page.
*
* @return void
*/
public static function render(): void {
// Handle export requests.
self::handle_export();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab switching only.
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'occupancy';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date filter.
$period = isset( $_GET['period'] ) ? sanitize_key( $_GET['period'] ) : 'this_month';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Custom date filter.
$start_date = isset( $_GET['start_date'] ) ? sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Custom date filter.
$end_date = isset( $_GET['end_date'] ) ? sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) : '';
$dates = self::get_date_range( $period, $start_date, $end_date );
?>
<div class="wrap">
<h1><?php esc_html_e( 'Reports', 'wp-bnb' ); ?></h1>
<nav class="nav-tab-wrapper wp-bnb-reports-tabs">
<a href="<?php echo esc_url( self::get_tab_url( 'occupancy', $period, $start_date, $end_date ) ); ?>"
class="nav-tab <?php echo 'occupancy' === $active_tab ? 'nav-tab-active' : ''; ?>">
<span class="dashicons dashicons-admin-home"></span>
<?php esc_html_e( 'Occupancy', 'wp-bnb' ); ?>
</a>
<a href="<?php echo esc_url( self::get_tab_url( 'revenue', $period, $start_date, $end_date ) ); ?>"
class="nav-tab <?php echo 'revenue' === $active_tab ? 'nav-tab-active' : ''; ?>">
<span class="dashicons dashicons-chart-area"></span>
<?php esc_html_e( 'Revenue', 'wp-bnb' ); ?>
</a>
<a href="<?php echo esc_url( self::get_tab_url( 'guests', $period, $start_date, $end_date ) ); ?>"
class="nav-tab <?php echo 'guests' === $active_tab ? 'nav-tab-active' : ''; ?>">
<span class="dashicons dashicons-groups"></span>
<?php esc_html_e( 'Guests', 'wp-bnb' ); ?>
</a>
</nav>
<div class="wp-bnb-reports-content">
<!-- Date Filter -->
<?php self::render_date_filter( $active_tab, $period, $start_date, $end_date, $dates ); ?>
<!-- Report Content -->
<div class="wp-bnb-report-body">
<?php
switch ( $active_tab ) {
case 'revenue':
self::render_revenue_report( $dates );
break;
case 'guests':
self::render_guests_report( $dates );
break;
default:
self::render_occupancy_report( $dates );
break;
}
?>
</div>
<!-- Export Buttons -->
<?php self::render_export_buttons( $active_tab, $period, $start_date, $end_date ); ?>
</div>
</div>
<?php
}
/**
* Get tab URL with filters preserved.
*
* @param string $tab Tab slug.
* @param string $period Period filter.
* @param string $start_date Custom start date.
* @param string $end_date Custom end date.
* @return string URL.
*/
private static function get_tab_url( string $tab, string $period, string $start_date, string $end_date ): string {
$args = array(
'page' => 'wp-bnb-reports',
'tab' => $tab,
);
if ( $period ) {
$args['period'] = $period;
}
if ( 'custom' === $period && $start_date && $end_date ) {
$args['start_date'] = $start_date;
$args['end_date'] = $end_date;
}
return add_query_arg( $args, admin_url( 'admin.php' ) );
}
/**
* Get date range based on period selection.
*
* @param string $period Period key.
* @param string $start_date Custom start date.
* @param string $end_date Custom end date.
* @return array{start: string, end: string, label: string}
*/
private static function get_date_range( string $period, string $start_date, string $end_date ): array {
switch ( $period ) {
case 'last_month':
return array(
'start' => gmdate( 'Y-m-01', strtotime( '-1 month' ) ),
'end' => gmdate( 'Y-m-t', strtotime( '-1 month' ) ),
'label' => gmdate( 'F Y', strtotime( '-1 month' ) ),
);
case 'this_year':
return array(
'start' => gmdate( 'Y-01-01' ),
'end' => gmdate( 'Y-m-d' ),
'label' => gmdate( 'Y' ),
);
case 'last_year':
return array(
'start' => gmdate( 'Y-01-01', strtotime( '-1 year' ) ),
'end' => gmdate( 'Y-12-31', strtotime( '-1 year' ) ),
'label' => gmdate( 'Y', strtotime( '-1 year' ) ),
);
case 'custom':
if ( $start_date && $end_date ) {
return array(
'start' => $start_date,
'end' => $end_date,
'label' => sprintf(
/* translators: 1: Start date, 2: End date */
__( '%1$s to %2$s', 'wp-bnb' ),
wp_date( get_option( 'date_format' ), strtotime( $start_date ) ),
wp_date( get_option( 'date_format' ), strtotime( $end_date ) )
),
);
}
// Fall through to this_month if custom dates not provided.
case 'this_month':
default:
return array(
'start' => gmdate( 'Y-m-01' ),
'end' => gmdate( 'Y-m-t' ),
'label' => gmdate( 'F Y' ),
);
}
}
/**
* Render date filter controls.
*
* @param string $active_tab Current tab.
* @param string $period Current period.
* @param string $start_date Custom start date.
* @param string $end_date Custom end date.
* @param array $dates Date range data.
* @return void
*/
private static function render_date_filter( string $active_tab, string $period, string $start_date, string $end_date, array $dates ): void {
?>
<div class="wp-bnb-reports-filters">
<form method="get" action="" class="wp-bnb-filter-form">
<input type="hidden" name="page" value="wp-bnb-reports">
<input type="hidden" name="tab" value="<?php echo esc_attr( $active_tab ); ?>">
<div class="wp-bnb-filter-group">
<label for="period"><?php esc_html_e( 'Period:', 'wp-bnb' ); ?></label>
<select name="period" id="period" class="wp-bnb-period-select">
<option value="this_month" <?php selected( $period, 'this_month' ); ?>><?php esc_html_e( 'This Month', 'wp-bnb' ); ?></option>
<option value="last_month" <?php selected( $period, 'last_month' ); ?>><?php esc_html_e( 'Last Month', 'wp-bnb' ); ?></option>
<option value="this_year" <?php selected( $period, 'this_year' ); ?>><?php esc_html_e( 'This Year', 'wp-bnb' ); ?></option>
<option value="last_year" <?php selected( $period, 'last_year' ); ?>><?php esc_html_e( 'Last Year', 'wp-bnb' ); ?></option>
<option value="custom" <?php selected( $period, 'custom' ); ?>><?php esc_html_e( 'Custom Range', 'wp-bnb' ); ?></option>
</select>
</div>
<div class="wp-bnb-filter-group wp-bnb-custom-dates" style="<?php echo 'custom' !== $period ? 'display: none;' : ''; ?>">
<label for="start_date"><?php esc_html_e( 'From:', 'wp-bnb' ); ?></label>
<input type="date" name="start_date" id="start_date" value="<?php echo esc_attr( $start_date ); ?>">
<label for="end_date"><?php esc_html_e( 'To:', 'wp-bnb' ); ?></label>
<input type="date" name="end_date" id="end_date" value="<?php echo esc_attr( $end_date ); ?>">
</div>
<button type="submit" class="button"><?php esc_html_e( 'Apply Filter', 'wp-bnb' ); ?></button>
</form>
<div class="wp-bnb-report-period">
<strong><?php esc_html_e( 'Showing:', 'wp-bnb' ); ?></strong>
<?php echo esc_html( $dates['label'] ); ?>
</div>
</div>
<?php
}
/**
* Render occupancy report.
*
* @param array $dates Date range.
* @return void
*/
private static function render_occupancy_report( array $dates ): void {
$data = self::get_occupancy_data( $dates['start'], $dates['end'] );
?>
<div class="wp-bnb-report-section">
<!-- Summary Cards -->
<div class="wp-bnb-summary-cards">
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( number_format( $data['overall_rate'], 1 ) ); ?>%</span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Overall Occupancy', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( number_format( $data['total_nights_booked'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Nights Booked', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( number_format( $data['total_nights_available'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Nights Available', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( count( $data['rooms'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Rooms', 'wp-bnb' ); ?></span>
</div>
</div>
<!-- Room Occupancy Table -->
<h3><?php esc_html_e( 'Occupancy by Room', 'wp-bnb' ); ?></h3>
<?php if ( empty( $data['rooms'] ) ) : ?>
<p class="wp-bnb-no-data"><?php esc_html_e( 'No rooms found.', 'wp-bnb' ); ?></p>
<?php else : ?>
<table class="wp-bnb-report-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Room', 'wp-bnb' ); ?></th>
<th><?php esc_html_e( 'Building', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Nights Booked', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Occupancy %', 'wp-bnb' ); ?></th>
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $data['rooms'] as $room ) : ?>
<tr>
<td>
<a href="<?php echo esc_url( get_edit_post_link( $room['id'] ) ); ?>">
<?php echo esc_html( $room['name'] ); ?>
</a>
</td>
<td><?php echo esc_html( $room['building'] ); ?></td>
<td class="num"><?php echo esc_html( $room['nights_booked'] ); ?></td>
<td class="num">
<div class="wp-bnb-progress-bar">
<div class="wp-bnb-progress-fill" style="width: <?php echo esc_attr( min( 100, $room['rate'] ) ); ?>%;"></div>
<span class="wp-bnb-progress-text"><?php echo esc_html( number_format( $room['rate'], 1 ) ); ?>%</span>
</div>
</td>
<td>
<?php
if ( $room['rate'] >= 80 ) {
echo '<span class="wp-bnb-status high">' . esc_html__( 'High', 'wp-bnb' ) . '</span>';
} elseif ( $room['rate'] >= 50 ) {
echo '<span class="wp-bnb-status medium">' . esc_html__( 'Medium', 'wp-bnb' ) . '</span>';
} else {
echo '<span class="wp-bnb-status low">' . esc_html__( 'Low', 'wp-bnb' ) . '</span>';
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<!-- Building Occupancy Table -->
<?php if ( ! empty( $data['buildings'] ) ) : ?>
<h3><?php esc_html_e( 'Occupancy by Building', 'wp-bnb' ); ?></h3>
<table class="wp-bnb-report-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Building', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Rooms', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Nights Booked', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Occupancy %', 'wp-bnb' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $data['buildings'] as $building ) : ?>
<tr>
<td>
<a href="<?php echo esc_url( get_edit_post_link( $building['id'] ) ); ?>">
<?php echo esc_html( $building['name'] ); ?>
</a>
</td>
<td class="num"><?php echo esc_html( $building['rooms'] ); ?></td>
<td class="num"><?php echo esc_html( $building['nights_booked'] ); ?></td>
<td class="num">
<div class="wp-bnb-progress-bar">
<div class="wp-bnb-progress-fill" style="width: <?php echo esc_attr( min( 100, $building['rate'] ) ); ?>%;"></div>
<span class="wp-bnb-progress-text"><?php echo esc_html( number_format( $building['rate'], 1 ) ); ?>%</span>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
/**
* Render revenue report.
*
* @param array $dates Date range.
* @return void
*/
private static function render_revenue_report( array $dates ): void {
$data = self::get_revenue_data( $dates['start'], $dates['end'] );
?>
<div class="wp-bnb-report-section">
<!-- Summary Cards -->
<div class="wp-bnb-summary-cards">
<div class="wp-bnb-summary-card primary">
<span class="wp-bnb-summary-value"><?php echo esc_html( Calculator::formatPrice( $data['total'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Total Revenue', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( Calculator::formatPrice( $data['room_revenue'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Room Revenue', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( Calculator::formatPrice( $data['services_revenue'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Services Revenue', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( $data['bookings_count'] ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Bookings', 'wp-bnb' ); ?></span>
</div>
</div>
<!-- Average Values -->
<div class="wp-bnb-summary-cards secondary">
<div class="wp-bnb-summary-card small">
<span class="wp-bnb-summary-value"><?php echo esc_html( Calculator::formatPrice( $data['avg_booking_value'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Avg. Booking Value', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card small">
<span class="wp-bnb-summary-value"><?php echo esc_html( Calculator::formatPrice( $data['avg_nightly_rate'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Avg. Nightly Rate', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card small">
<span class="wp-bnb-summary-value"><?php echo esc_html( number_format( $data['avg_nights'], 1 ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Avg. Stay Length', 'wp-bnb' ); ?></span>
</div>
</div>
<!-- Revenue by Room -->
<h3><?php esc_html_e( 'Revenue by Room', 'wp-bnb' ); ?></h3>
<?php if ( empty( $data['by_room'] ) ) : ?>
<p class="wp-bnb-no-data"><?php esc_html_e( 'No revenue data for this period.', 'wp-bnb' ); ?></p>
<?php else : ?>
<table class="wp-bnb-report-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Room', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Bookings', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Nights', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Revenue', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( '% of Total', 'wp-bnb' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $data['by_room'] as $room ) : ?>
<tr>
<td>
<a href="<?php echo esc_url( get_edit_post_link( $room['id'] ) ); ?>">
<?php echo esc_html( $room['name'] ); ?>
</a>
</td>
<td class="num"><?php echo esc_html( $room['bookings'] ); ?></td>
<td class="num"><?php echo esc_html( $room['nights'] ); ?></td>
<td class="num"><?php echo esc_html( Calculator::formatPrice( $room['revenue'] ) ); ?></td>
<td class="num"><?php echo esc_html( number_format( $room['percentage'], 1 ) ); ?>%</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th><?php esc_html_e( 'Total', 'wp-bnb' ); ?></th>
<th class="num"><?php echo esc_html( $data['bookings_count'] ); ?></th>
<th class="num"><?php echo esc_html( $data['total_nights'] ); ?></th>
<th class="num"><?php echo esc_html( Calculator::formatPrice( $data['total'] ) ); ?></th>
<th class="num">100%</th>
</tr>
</tfoot>
</table>
<?php endif; ?>
<!-- Revenue by Pricing Tier -->
<?php if ( ! empty( $data['by_tier'] ) ) : ?>
<h3><?php esc_html_e( 'Revenue by Pricing Tier', 'wp-bnb' ); ?></h3>
<table class="wp-bnb-report-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Tier', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Bookings', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Revenue', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( '% of Total', 'wp-bnb' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $data['by_tier'] as $tier_key => $tier ) : ?>
<tr>
<td><?php echo esc_html( $tier['label'] ); ?></td>
<td class="num"><?php echo esc_html( $tier['bookings'] ); ?></td>
<td class="num"><?php echo esc_html( Calculator::formatPrice( $tier['revenue'] ) ); ?></td>
<td class="num"><?php echo esc_html( number_format( $tier['percentage'], 1 ) ); ?>%</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
/**
* Render guests report.
*
* @param array $dates Date range.
* @return void
*/
private static function render_guests_report( array $dates ): void {
$data = self::get_guests_data( $dates['start'], $dates['end'] );
?>
<div class="wp-bnb-report-section">
<!-- Summary Cards -->
<div class="wp-bnb-summary-cards">
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( $data['total_guests'] ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Total Guests', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( $data['new_guests'] ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'New Guests', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( $data['repeat_guests'] ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Repeat Guests', 'wp-bnb' ); ?></span>
</div>
<div class="wp-bnb-summary-card">
<span class="wp-bnb-summary-value"><?php echo esc_html( Calculator::formatPrice( $data['avg_guest_value'] ) ); ?></span>
<span class="wp-bnb-summary-label"><?php esc_html_e( 'Avg. Guest Value', 'wp-bnb' ); ?></span>
</div>
</div>
<!-- Top Guests -->
<h3><?php esc_html_e( 'Top Guests by Revenue', 'wp-bnb' ); ?></h3>
<?php if ( empty( $data['top_guests'] ) ) : ?>
<p class="wp-bnb-no-data"><?php esc_html_e( 'No guest data for this period.', 'wp-bnb' ); ?></p>
<?php else : ?>
<table class="wp-bnb-report-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Guest', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Bookings', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Nights', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Total Spent', 'wp-bnb' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $data['top_guests'] as $guest ) : ?>
<tr>
<td>
<?php if ( $guest['id'] ) : ?>
<a href="<?php echo esc_url( get_edit_post_link( $guest['id'] ) ); ?>">
<?php echo esc_html( $guest['name'] ); ?>
</a>
<?php else : ?>
<?php echo esc_html( $guest['name'] ); ?>
<?php endif; ?>
</td>
<td class="num"><?php echo esc_html( $guest['bookings'] ); ?></td>
<td class="num"><?php echo esc_html( $guest['nights'] ); ?></td>
<td class="num"><?php echo esc_html( Calculator::formatPrice( $guest['total_spent'] ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<!-- Guest Nationality Breakdown -->
<?php if ( ! empty( $data['by_nationality'] ) ) : ?>
<h3><?php esc_html_e( 'Guests by Nationality', 'wp-bnb' ); ?></h3>
<table class="wp-bnb-report-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Nationality', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( 'Guests', 'wp-bnb' ); ?></th>
<th class="num"><?php esc_html_e( '% of Total', 'wp-bnb' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $data['by_nationality'] as $nationality ) : ?>
<tr>
<td><?php echo esc_html( $nationality['name'] ); ?></td>
<td class="num"><?php echo esc_html( $nationality['count'] ); ?></td>
<td class="num"><?php echo esc_html( number_format( $nationality['percentage'], 1 ) ); ?>%</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
/**
* Render export buttons.
*
* @param string $tab Current tab.
* @param string $period Period filter.
* @param string $start_date Custom start date.
* @param string $end_date Custom end date.
* @return void
*/
private static function render_export_buttons( string $tab, string $period, string $start_date, string $end_date ): void {
$export_nonce = wp_create_nonce( 'wp_bnb_export_report' );
?>
<div class="wp-bnb-export-buttons">
<h4><?php esc_html_e( 'Export Report', 'wp-bnb' ); ?></h4>
<div class="wp-bnb-export-actions">
<a href="<?php echo esc_url( self::get_export_url( 'csv', $tab, $period, $start_date, $end_date, $export_nonce ) ); ?>"
class="button">
<span class="dashicons dashicons-media-spreadsheet"></span>
<?php esc_html_e( 'Export CSV', 'wp-bnb' ); ?>
</a>
<a href="<?php echo esc_url( self::get_export_url( 'pdf', $tab, $period, $start_date, $end_date, $export_nonce ) ); ?>"
class="button">
<span class="dashicons dashicons-pdf"></span>
<?php esc_html_e( 'Export PDF', 'wp-bnb' ); ?>
</a>
</div>
</div>
<?php
}
/**
* Get export URL.
*
* @param string $format Export format (csv|pdf).
* @param string $tab Report tab.
* @param string $period Period filter.
* @param string $start_date Custom start date.
* @param string $end_date Custom end date.
* @param string $nonce Security nonce.
* @return string Export URL.
*/
private static function get_export_url( string $format, string $tab, string $period, string $start_date, string $end_date, string $nonce ): string {
$args = array(
'page' => 'wp-bnb-reports',
'tab' => $tab,
'period' => $period,
'export' => $format,
'_wpnonce' => $nonce,
);
if ( 'custom' === $period ) {
$args['start_date'] = $start_date;
$args['end_date'] = $end_date;
}
return add_query_arg( $args, admin_url( 'admin.php' ) );
}
/**
* Handle export requests.
*
* @return void
*/
private static function handle_export(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below.
if ( ! isset( $_GET['export'] ) ) {
return;
}
// Verify nonce.
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wp_bnb_export_report' ) ) {
wp_die( esc_html__( 'Security check failed.', 'wp-bnb' ) );
}
// Check permissions.
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to export reports.', 'wp-bnb' ) );
}
$format = sanitize_key( wp_unslash( $_GET['export'] ) );
$tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'occupancy';
$period = isset( $_GET['period'] ) ? sanitize_key( $_GET['period'] ) : 'this_month';
$start_date = isset( $_GET['start_date'] ) ? sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) : '';
$end_date = isset( $_GET['end_date'] ) ? sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) : '';
$dates = self::get_date_range( $period, $start_date, $end_date );
if ( 'csv' === $format ) {
self::export_csv( $tab, $dates );
} elseif ( 'pdf' === $format ) {
self::export_pdf( $tab, $dates );
}
}
/**
* Export report as CSV.
*
* @param string $tab Report tab.
* @param array $dates Date range.
* @return void
*/
private static function export_csv( string $tab, array $dates ): void {
$filename = sprintf( 'wp-bnb-%s-report-%s.csv', $tab, gmdate( 'Y-m-d' ) );
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename=' . $filename );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
$output = fopen( 'php://output', 'w' );
// Add BOM for Excel compatibility.
fwrite( $output, "\xEF\xBB\xBF" );
switch ( $tab ) {
case 'revenue':
self::export_revenue_csv( $output, $dates );
break;
case 'guests':
self::export_guests_csv( $output, $dates );
break;
default:
self::export_occupancy_csv( $output, $dates );
break;
}
fclose( $output );
exit;
}
/**
* Export occupancy report as CSV.
*
* @param resource $output File handle.
* @param array $dates Date range.
* @return void
*/
private static function export_occupancy_csv( $output, array $dates ): void {
$data = self::get_occupancy_data( $dates['start'], $dates['end'] );
// Report header.
fputcsv( $output, array( __( 'Occupancy Report', 'wp-bnb' ), $dates['label'] ) );
fputcsv( $output, array() );
// Summary.
fputcsv( $output, array( __( 'Summary', 'wp-bnb' ) ) );
fputcsv( $output, array( __( 'Overall Occupancy', 'wp-bnb' ), number_format( $data['overall_rate'], 1 ) . '%' ) );
fputcsv( $output, array( __( 'Nights Booked', 'wp-bnb' ), $data['total_nights_booked'] ) );
fputcsv( $output, array( __( 'Nights Available', 'wp-bnb' ), $data['total_nights_available'] ) );
fputcsv( $output, array() );
// Room data.
fputcsv( $output, array( __( 'Room', 'wp-bnb' ), __( 'Building', 'wp-bnb' ), __( 'Nights Booked', 'wp-bnb' ), __( 'Occupancy %', 'wp-bnb' ) ) );
foreach ( $data['rooms'] as $room ) {
fputcsv( $output, array( $room['name'], $room['building'], $room['nights_booked'], number_format( $room['rate'], 1 ) . '%' ) );
}
}
/**
* Export revenue report as CSV.
*
* @param resource $output File handle.
* @param array $dates Date range.
* @return void
*/
private static function export_revenue_csv( $output, array $dates ): void {
$data = self::get_revenue_data( $dates['start'], $dates['end'] );
// Report header.
fputcsv( $output, array( __( 'Revenue Report', 'wp-bnb' ), $dates['label'] ) );
fputcsv( $output, array() );
// Summary.
fputcsv( $output, array( __( 'Summary', 'wp-bnb' ) ) );
fputcsv( $output, array( __( 'Total Revenue', 'wp-bnb' ), Calculator::formatPrice( $data['total'] ) ) );
fputcsv( $output, array( __( 'Room Revenue', 'wp-bnb' ), Calculator::formatPrice( $data['room_revenue'] ) ) );
fputcsv( $output, array( __( 'Services Revenue', 'wp-bnb' ), Calculator::formatPrice( $data['services_revenue'] ) ) );
fputcsv( $output, array( __( 'Bookings', 'wp-bnb' ), $data['bookings_count'] ) );
fputcsv( $output, array() );
// By room.
fputcsv( $output, array( __( 'Room', 'wp-bnb' ), __( 'Bookings', 'wp-bnb' ), __( 'Nights', 'wp-bnb' ), __( 'Revenue', 'wp-bnb' ), __( '% of Total', 'wp-bnb' ) ) );
foreach ( $data['by_room'] as $room ) {
fputcsv( $output, array( $room['name'], $room['bookings'], $room['nights'], Calculator::formatPrice( $room['revenue'] ), number_format( $room['percentage'], 1 ) . '%' ) );
}
}
/**
* Export guests report as CSV.
*
* @param resource $output File handle.
* @param array $dates Date range.
* @return void
*/
private static function export_guests_csv( $output, array $dates ): void {
$data = self::get_guests_data( $dates['start'], $dates['end'] );
// Report header.
fputcsv( $output, array( __( 'Guests Report', 'wp-bnb' ), $dates['label'] ) );
fputcsv( $output, array() );
// Summary.
fputcsv( $output, array( __( 'Summary', 'wp-bnb' ) ) );
fputcsv( $output, array( __( 'Total Guests', 'wp-bnb' ), $data['total_guests'] ) );
fputcsv( $output, array( __( 'New Guests', 'wp-bnb' ), $data['new_guests'] ) );
fputcsv( $output, array( __( 'Repeat Guests', 'wp-bnb' ), $data['repeat_guests'] ) );
fputcsv( $output, array() );
// Top guests.
fputcsv( $output, array( __( 'Guest', 'wp-bnb' ), __( 'Bookings', 'wp-bnb' ), __( 'Nights', 'wp-bnb' ), __( 'Total Spent', 'wp-bnb' ) ) );
foreach ( $data['top_guests'] as $guest ) {
fputcsv( $output, array( $guest['name'], $guest['bookings'], $guest['nights'], Calculator::formatPrice( $guest['total_spent'] ) ) );
}
}
/**
* Export report as PDF.
*
* @param string $tab Report tab.
* @param array $dates Date range.
* @return void
*/
private static function export_pdf( string $tab, array $dates ): void {
$filename = sprintf( 'wp-bnb-%s-report-%s.pdf', $tab, gmdate( 'Y-m-d' ) );
// Get site info.
$site_name = get_bloginfo( 'name' );
// Build HTML content.
$html = self::get_pdf_html( $tab, $dates, $site_name );
// Generate PDF using mPDF.
try {
// Use WordPress temp directory for mPDF.
$temp_dir = get_temp_dir() . 'mpdf';
if ( ! file_exists( $temp_dir ) ) {
wp_mkdir_p( $temp_dir );
}
$mpdf = new Mpdf(
array(
'mode' => 'utf-8',
'format' => 'A4',
'margin_left' => 15,
'margin_right' => 15,
'margin_top' => 15,
'margin_bottom' => 15,
'tempDir' => $temp_dir,
)
);
$mpdf->SetTitle( sprintf( '%s - %s Report', $site_name, ucfirst( $tab ) ) );
$mpdf->SetAuthor( $site_name );
$mpdf->SetCreator( 'WP BnB' );
$mpdf->WriteHTML( $html );
$mpdf->Output( $filename, 'D' );
} catch ( \Exception $e ) {
wp_die( esc_html( sprintf( __( 'PDF generation failed: %s', 'wp-bnb' ), $e->getMessage() ) ) );
}
exit;
}
/**
* Get PDF HTML content.
*
* @param string $tab Report tab.
* @param array $dates Date range.
* @param string $site_name Site name.
* @return string HTML content.
*/
private static function get_pdf_html( string $tab, array $dates, string $site_name ): string {
$title = '';
switch ( $tab ) {
case 'revenue':
$title = __( 'Revenue Report', 'wp-bnb' );
$data = self::get_revenue_data( $dates['start'], $dates['end'] );
break;
case 'guests':
$title = __( 'Guests Report', 'wp-bnb' );
$data = self::get_guests_data( $dates['start'], $dates['end'] );
break;
default:
$title = __( 'Occupancy Report', 'wp-bnb' );
$data = self::get_occupancy_data( $dates['start'], $dates['end'] );
break;
}
$html = '<html><head><style>
body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; color: #333; }
h1 { font-size: 18pt; color: #2271b1; margin-bottom: 5pt; }
h2 { font-size: 14pt; color: #1d2327; margin-top: 20pt; margin-bottom: 10pt; border-bottom: 1px solid #c3c4c7; padding-bottom: 5pt; }
h3 { font-size: 12pt; color: #50575e; margin-top: 15pt; }
.subtitle { font-size: 11pt; color: #787c82; margin-bottom: 15pt; }
.summary-grid { margin: 15pt 0; }
.summary-item { display: inline-block; width: 23%; text-align: center; padding: 10pt; background: #f6f7f7; margin-right: 2%; }
.summary-value { font-size: 16pt; font-weight: bold; color: #2271b1; display: block; }
.summary-label { font-size: 8pt; color: #787c82; }
table { width: 100%; border-collapse: collapse; margin-top: 10pt; }
th { background: #f6f7f7; text-align: left; padding: 8pt; font-size: 9pt; border-bottom: 2px solid #c3c4c7; }
td { padding: 6pt 8pt; border-bottom: 1px solid #e1e4e8; font-size: 9pt; }
.num { text-align: right; }
.footer { margin-top: 30pt; padding-top: 10pt; border-top: 1px solid #c3c4c7; font-size: 8pt; color: #787c82; text-align: center; }
</style></head><body>';
$html .= '<h1>' . esc_html( $title ) . '</h1>';
$html .= '<div class="subtitle">' . esc_html( $site_name ) . ' | ' . esc_html( $dates['label'] ) . '</div>';
// Generate report-specific content.
switch ( $tab ) {
case 'revenue':
$html .= self::get_revenue_pdf_content( $data );
break;
case 'guests':
$html .= self::get_guests_pdf_content( $data );
break;
default:
$html .= self::get_occupancy_pdf_content( $data );
break;
}
$html .= '<div class="footer">' . sprintf(
/* translators: 1: Site name, 2: Current date */
esc_html__( 'Generated by WP BnB for %1$s on %2$s', 'wp-bnb' ),
$site_name,
wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) )
) . '</div>';
$html .= '</body></html>';
return $html;
}
/**
* Get occupancy PDF content.
*
* @param array $data Report data.
* @return string HTML content.
*/
private static function get_occupancy_pdf_content( array $data ): string {
$html = '<h2>' . esc_html__( 'Summary', 'wp-bnb' ) . '</h2>';
$html .= '<table><tr>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;color:#2271b1;">' . number_format( $data['overall_rate'], 1 ) . '%</strong><br><small>' . esc_html__( 'Occupancy', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . number_format( $data['total_nights_booked'] ) . '</strong><br><small>' . esc_html__( 'Nights Booked', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . number_format( $data['total_nights_available'] ) . '</strong><br><small>' . esc_html__( 'Available', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . count( $data['rooms'] ) . '</strong><br><small>' . esc_html__( 'Rooms', 'wp-bnb' ) . '</small></td>';
$html .= '</tr></table>';
if ( ! empty( $data['rooms'] ) ) {
$html .= '<h2>' . esc_html__( 'Occupancy by Room', 'wp-bnb' ) . '</h2>';
$html .= '<table><thead><tr><th>' . esc_html__( 'Room', 'wp-bnb' ) . '</th><th>' . esc_html__( 'Building', 'wp-bnb' ) . '</th><th class="num">' . esc_html__( 'Nights', 'wp-bnb' ) . '</th><th class="num">' . esc_html__( 'Rate', 'wp-bnb' ) . '</th></tr></thead><tbody>';
foreach ( $data['rooms'] as $room ) {
$html .= '<tr><td>' . esc_html( $room['name'] ) . '</td><td>' . esc_html( $room['building'] ) . '</td><td class="num">' . $room['nights_booked'] . '</td><td class="num">' . number_format( $room['rate'], 1 ) . '%</td></tr>';
}
$html .= '</tbody></table>';
}
return $html;
}
/**
* Get revenue PDF content.
*
* @param array $data Report data.
* @return string HTML content.
*/
private static function get_revenue_pdf_content( array $data ): string {
$html = '<h2>' . esc_html__( 'Summary', 'wp-bnb' ) . '</h2>';
$html .= '<table><tr>';
$html .= '<td style="width:25%;text-align:center;background:#d4edda;"><strong style="font-size:14pt;color:#00a32a;">' . esc_html( Calculator::formatPrice( $data['total'] ) ) . '</strong><br><small>' . esc_html__( 'Total', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . esc_html( Calculator::formatPrice( $data['room_revenue'] ) ) . '</strong><br><small>' . esc_html__( 'Rooms', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . esc_html( Calculator::formatPrice( $data['services_revenue'] ) ) . '</strong><br><small>' . esc_html__( 'Services', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . $data['bookings_count'] . '</strong><br><small>' . esc_html__( 'Bookings', 'wp-bnb' ) . '</small></td>';
$html .= '</tr></table>';
if ( ! empty( $data['by_room'] ) ) {
$html .= '<h2>' . esc_html__( 'Revenue by Room', 'wp-bnb' ) . '</h2>';
$html .= '<table><thead><tr><th>' . esc_html__( 'Room', 'wp-bnb' ) . '</th><th class="num">' . esc_html__( 'Bookings', 'wp-bnb' ) . '</th><th class="num">' . esc_html__( 'Nights', 'wp-bnb' ) . '</th><th class="num">' . esc_html__( 'Revenue', 'wp-bnb' ) . '</th><th class="num">%</th></tr></thead><tbody>';
foreach ( $data['by_room'] as $room ) {
$html .= '<tr><td>' . esc_html( $room['name'] ) . '</td><td class="num">' . $room['bookings'] . '</td><td class="num">' . $room['nights'] . '</td><td class="num">' . esc_html( Calculator::formatPrice( $room['revenue'] ) ) . '</td><td class="num">' . number_format( $room['percentage'], 1 ) . '%</td></tr>';
}
$html .= '</tbody></table>';
}
return $html;
}
/**
* Get guests PDF content.
*
* @param array $data Report data.
* @return string HTML content.
*/
private static function get_guests_pdf_content( array $data ): string {
$html = '<h2>' . esc_html__( 'Summary', 'wp-bnb' ) . '</h2>';
$html .= '<table><tr>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;color:#2271b1;">' . $data['total_guests'] . '</strong><br><small>' . esc_html__( 'Total', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . $data['new_guests'] . '</strong><br><small>' . esc_html__( 'New', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . $data['repeat_guests'] . '</strong><br><small>' . esc_html__( 'Repeat', 'wp-bnb' ) . '</small></td>';
$html .= '<td style="width:25%;text-align:center;background:#f6f7f7;"><strong style="font-size:14pt;">' . esc_html( Calculator::formatPrice( $data['avg_guest_value'] ) ) . '</strong><br><small>' . esc_html__( 'Avg Value', 'wp-bnb' ) . '</small></td>';
$html .= '</tr></table>';
if ( ! empty( $data['top_guests'] ) ) {
$html .= '<h2>' . esc_html__( 'Top Guests', 'wp-bnb' ) . '</h2>';
$html .= '<table><thead><tr><th>' . esc_html__( 'Guest', 'wp-bnb' ) . '</th><th class="num">' . esc_html__( 'Bookings', 'wp-bnb' ) . '</th><th class="num">' . esc_html__( 'Nights', 'wp-bnb' ) . '</th><th class="num">' . esc_html__( 'Spent', 'wp-bnb' ) . '</th></tr></thead><tbody>';
foreach ( $data['top_guests'] as $guest ) {
$html .= '<tr><td>' . esc_html( $guest['name'] ) . '</td><td class="num">' . $guest['bookings'] . '</td><td class="num">' . $guest['nights'] . '</td><td class="num">' . esc_html( Calculator::formatPrice( $guest['total_spent'] ) ) . '</td></tr>';
}
$html .= '</tbody></table>';
}
return $html;
}
/**
* Get occupancy data for a date range.
*
* @param string $start_date Start date (Y-m-d).
* @param string $end_date End date (Y-m-d).
* @return array Report data.
*/
public static function get_occupancy_data( string $start_date, string $end_date ): array {
$rooms = get_posts(
array(
'post_type' => Room::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
)
);
$days_in_period = max( 1, Booking::calculate_nights( $start_date, $end_date ) );
$total_rooms = count( $rooms );
$total_nights_available = $total_rooms * $days_in_period;
$room_data = array();
$building_data = array();
$total_nights_booked = 0;
foreach ( $rooms as $room ) {
$building = Room::get_building( $room->ID );
$building_id = $building ? $building->ID : 0;
// Get bookings for this room in the period.
$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_room_id',
'value' => $room->ID,
),
array(
'key' => '_bnb_booking_status',
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
'compare' => 'IN',
),
array(
'key' => '_bnb_booking_check_in',
'value' => $end_date,
'compare' => '<=',
'type' => 'DATE',
),
array(
'key' => '_bnb_booking_check_out',
'value' => $start_date,
'compare' => '>=',
'type' => 'DATE',
),
),
)
);
$nights_booked = 0;
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 period boundaries.
$effective_start = max( $check_in, $start_date );
$effective_end = min( $check_out, gmdate( 'Y-m-d', strtotime( $end_date . ' +1 day' ) ) );
$nights = Booking::calculate_nights( $effective_start, $effective_end );
$nights_booked += $nights;
}
$rate = $days_in_period > 0 ? ( $nights_booked / $days_in_period ) * 100 : 0;
$total_nights_booked += $nights_booked;
$room_data[] = array(
'id' => $room->ID,
'name' => $room->post_title,
'building' => $building ? $building->post_title : __( 'Unassigned', 'wp-bnb' ),
'building_id' => $building_id,
'nights_booked' => $nights_booked,
'rate' => $rate,
);
// Aggregate building data.
if ( $building_id ) {
if ( ! isset( $building_data[ $building_id ] ) ) {
$building_data[ $building_id ] = array(
'id' => $building_id,
'name' => $building->post_title,
'rooms' => 0,
'nights_booked' => 0,
'total_nights' => 0,
);
}
$building_data[ $building_id ]['rooms']++;
$building_data[ $building_id ]['nights_booked'] += $nights_booked;
$building_data[ $building_id ]['total_nights'] += $days_in_period;
}
}
// Calculate building rates.
foreach ( $building_data as &$building ) {
$building['rate'] = $building['total_nights'] > 0
? ( $building['nights_booked'] / $building['total_nights'] ) * 100
: 0;
}
// Sort rooms by occupancy rate descending.
usort( $room_data, fn( $a, $b ) => $b['rate'] <=> $a['rate'] );
$overall_rate = $total_nights_available > 0
? ( $total_nights_booked / $total_nights_available ) * 100
: 0;
return array(
'overall_rate' => $overall_rate,
'total_nights_booked' => $total_nights_booked,
'total_nights_available' => $total_nights_available,
'rooms' => $room_data,
'buildings' => array_values( $building_data ),
);
}
/**
* Get revenue data for a date range.
*
* @param string $start_date Start date (Y-m-d).
* @param string $end_date End date (Y-m-d).
* @return array Report data.
*/
public static function get_revenue_data( string $start_date, string $end_date ): array {
$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;
$room_revenue = 0.0;
$services_revenue = 0.0;
$total_nights = 0;
$by_room = array();
$by_tier = array(
'short_term' => array(
'label' => __( 'Short-term (1-6 nights)', 'wp-bnb' ),
'bookings' => 0,
'revenue' => 0,
'percentage' => 0,
),
'mid_term' => array(
'label' => __( 'Mid-term (7-29 nights)', 'wp-bnb' ),
'bookings' => 0,
'revenue' => 0,
'percentage' => 0,
),
'long_term' => array(
'label' => __( 'Long-term (30+ nights)', 'wp-bnb' ),
'bookings' => 0,
'revenue' => 0,
'percentage' => 0,
),
);
foreach ( $bookings as $booking ) {
$room_id = (int) get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
$room_price = (float) get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
$nights = Booking::calculate_nights( $check_in, $check_out );
$svc_total = Booking::calculate_booking_services_total( $booking->ID );
$booking_total = $room_price + $svc_total;
$total += $booking_total;
$room_revenue += $room_price;
$services_revenue += $svc_total;
$total_nights += $nights;
// By room.
if ( ! isset( $by_room[ $room_id ] ) ) {
$room = get_post( $room_id );
$by_room[ $room_id ] = array(
'id' => $room_id,
'name' => $room ? $room->post_title : __( 'Unknown Room', 'wp-bnb' ),
'bookings' => 0,
'nights' => 0,
'revenue' => 0,
);
}
$by_room[ $room_id ]['bookings']++;
$by_room[ $room_id ]['nights'] += $nights;
$by_room[ $room_id ]['revenue'] += $booking_total;
// By tier.
if ( $nights < 7 ) {
$tier = 'short_term';
} elseif ( $nights < 30 ) {
$tier = 'mid_term';
} else {
$tier = 'long_term';
}
$by_tier[ $tier ]['bookings']++;
$by_tier[ $tier ]['revenue'] += $booking_total;
}
// Calculate percentages.
foreach ( $by_room as &$room ) {
$room['percentage'] = $total > 0 ? ( $room['revenue'] / $total ) * 100 : 0;
}
foreach ( $by_tier as &$tier ) {
$tier['percentage'] = $total > 0 ? ( $tier['revenue'] / $total ) * 100 : 0;
}
// Sort by revenue descending.
usort( $by_room, fn( $a, $b ) => $b['revenue'] <=> $a['revenue'] );
$bookings_count = count( $bookings );
$avg_booking_value = $bookings_count > 0 ? $total / $bookings_count : 0;
$avg_nightly_rate = $total_nights > 0 ? $room_revenue / $total_nights : 0;
$avg_nights = $bookings_count > 0 ? $total_nights / $bookings_count : 0;
return array(
'total' => $total,
'room_revenue' => $room_revenue,
'services_revenue' => $services_revenue,
'bookings_count' => $bookings_count,
'total_nights' => $total_nights,
'avg_booking_value' => $avg_booking_value,
'avg_nightly_rate' => $avg_nightly_rate,
'avg_nights' => $avg_nights,
'by_room' => $by_room,
'by_tier' => $by_tier,
);
}
/**
* Get guests data for a date range.
*
* @param string $start_date Start date (Y-m-d).
* @param string $end_date End date (Y-m-d).
* @return array Report data.
*/
public static function get_guests_data( string $start_date, string $end_date ): array {
// Get all guests.
$all_guests = get_posts(
array(
'post_type' => Guest::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
)
);
// Get guests created in the period.
$new_guests = get_posts(
array(
'post_type' => Guest::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'date_query' => array(
array(
'after' => $start_date,
'before' => $end_date,
'inclusive' => true,
),
),
)
);
// Get bookings in the period with guest data.
$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',
),
),
)
);
// Aggregate guest data from bookings.
$guest_stats = array();
$total_revenue = 0.0;
foreach ( $bookings as $booking ) {
$guest_id = (int) get_post_meta( $booking->ID, '_bnb_booking_guest_id', true );
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
$price = (float) get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
$svc_total = Booking::calculate_booking_services_total( $booking->ID );
$nights = Booking::calculate_nights( $check_in, $check_out );
$booking_total = $price + $svc_total;
$total_revenue += $booking_total;
$key = $guest_id ?: 'anon_' . sanitize_title( $guest_name );
if ( ! isset( $guest_stats[ $key ] ) ) {
$guest_stats[ $key ] = array(
'id' => $guest_id,
'name' => $guest_name ?: __( 'Unknown', 'wp-bnb' ),
'bookings' => 0,
'nights' => 0,
'total_spent' => 0,
);
}
$guest_stats[ $key ]['bookings']++;
$guest_stats[ $key ]['nights'] += $nights;
$guest_stats[ $key ]['total_spent'] += $booking_total;
}
// Count repeat guests (2+ bookings ever).
$repeat_count = 0;
foreach ( $all_guests as $guest ) {
$booking_count = Guest::get_booking_count( $guest->ID );
if ( $booking_count >= 2 ) {
++$repeat_count;
}
}
// Sort by total spent and get top 10.
usort( $guest_stats, fn( $a, $b ) => $b['total_spent'] <=> $a['total_spent'] );
$top_guests = array_slice( $guest_stats, 0, 10 );
// Get nationality breakdown.
$by_nationality = array();
foreach ( $all_guests as $guest ) {
$nationality = get_post_meta( $guest->ID, '_bnb_guest_nationality', true );
if ( ! $nationality ) {
$nationality = __( 'Not specified', 'wp-bnb' );
}
if ( ! isset( $by_nationality[ $nationality ] ) ) {
$by_nationality[ $nationality ] = array(
'name' => $nationality,
'count' => 0,
);
}
$by_nationality[ $nationality ]['count']++;
}
// Calculate percentages and sort.
$total_guests = count( $all_guests );
foreach ( $by_nationality as &$nat ) {
$nat['percentage'] = $total_guests > 0 ? ( $nat['count'] / $total_guests ) * 100 : 0;
}
usort( $by_nationality, fn( $a, $b ) => $b['count'] <=> $a['count'] );
$guest_count_in_period = count( array_unique( array_keys( $guest_stats ) ) );
$avg_guest_value = $guest_count_in_period > 0 ? $total_revenue / $guest_count_in_period : 0;
return array(
'total_guests' => $total_guests,
'new_guests' => count( $new_guests ),
'repeat_guests' => $repeat_count,
'avg_guest_value' => $avg_guest_value,
'top_guests' => $top_guests,
'by_nationality' => array_slice( $by_nationality, 0, 10 ),
);
}
}