1369 lines
50 KiB
PHP
1369 lines
50 KiB
PHP
|
|
<?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 ),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|