'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 { ?>
%

%
= 80 ) { echo '' . esc_html__( 'High', 'wp-bnb' ) . ''; } elseif ( $room['rate'] >= 50 ) { echo '' . esc_html__( 'Medium', 'wp-bnb' ) . ''; } else { echo '' . esc_html__( 'Low', 'wp-bnb' ) . ''; } ?>

%

%
100%

$tier ) : ?>
%

%

'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 .= '

' . esc_html( $title ) . '

'; $html .= '
' . esc_html( $site_name ) . ' | ' . esc_html( $dates['label'] ) . '
'; // 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 .= ''; $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 = '

' . esc_html__( 'Summary', 'wp-bnb' ) . '

'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
' . number_format( $data['overall_rate'], 1 ) . '%
' . esc_html__( 'Occupancy', 'wp-bnb' ) . '
' . number_format( $data['total_nights_booked'] ) . '
' . esc_html__( 'Nights Booked', 'wp-bnb' ) . '
' . number_format( $data['total_nights_available'] ) . '
' . esc_html__( 'Available', 'wp-bnb' ) . '
' . count( $data['rooms'] ) . '
' . esc_html__( 'Rooms', 'wp-bnb' ) . '
'; if ( ! empty( $data['rooms'] ) ) { $html .= '

' . esc_html__( 'Occupancy by Room', 'wp-bnb' ) . '

'; $html .= ''; foreach ( $data['rooms'] as $room ) { $html .= ''; } $html .= '
' . esc_html__( 'Room', 'wp-bnb' ) . '' . esc_html__( 'Building', 'wp-bnb' ) . '' . esc_html__( 'Nights', 'wp-bnb' ) . '' . esc_html__( 'Rate', 'wp-bnb' ) . '
' . esc_html( $room['name'] ) . '' . esc_html( $room['building'] ) . '' . $room['nights_booked'] . '' . number_format( $room['rate'], 1 ) . '%
'; } 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 = '

' . esc_html__( 'Summary', 'wp-bnb' ) . '

'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
' . esc_html( Calculator::formatPrice( $data['total'] ) ) . '
' . esc_html__( 'Total', 'wp-bnb' ) . '
' . esc_html( Calculator::formatPrice( $data['room_revenue'] ) ) . '
' . esc_html__( 'Rooms', 'wp-bnb' ) . '
' . esc_html( Calculator::formatPrice( $data['services_revenue'] ) ) . '
' . esc_html__( 'Services', 'wp-bnb' ) . '
' . $data['bookings_count'] . '
' . esc_html__( 'Bookings', 'wp-bnb' ) . '
'; if ( ! empty( $data['by_room'] ) ) { $html .= '

' . esc_html__( 'Revenue by Room', 'wp-bnb' ) . '

'; $html .= ''; foreach ( $data['by_room'] as $room ) { $html .= ''; } $html .= '
' . esc_html__( 'Room', 'wp-bnb' ) . '' . esc_html__( 'Bookings', 'wp-bnb' ) . '' . esc_html__( 'Nights', 'wp-bnb' ) . '' . esc_html__( 'Revenue', 'wp-bnb' ) . '%
' . esc_html( $room['name'] ) . '' . $room['bookings'] . '' . $room['nights'] . '' . esc_html( Calculator::formatPrice( $room['revenue'] ) ) . '' . number_format( $room['percentage'], 1 ) . '%
'; } 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 = '

' . esc_html__( 'Summary', 'wp-bnb' ) . '

'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
' . $data['total_guests'] . '
' . esc_html__( 'Total', 'wp-bnb' ) . '
' . $data['new_guests'] . '
' . esc_html__( 'New', 'wp-bnb' ) . '
' . $data['repeat_guests'] . '
' . esc_html__( 'Repeat', 'wp-bnb' ) . '
' . esc_html( Calculator::formatPrice( $data['avg_guest_value'] ) ) . '
' . esc_html__( 'Avg Value', 'wp-bnb' ) . '
'; if ( ! empty( $data['top_guests'] ) ) { $html .= '

' . esc_html__( 'Top Guests', 'wp-bnb' ) . '

'; $html .= ''; foreach ( $data['top_guests'] as $guest ) { $html .= ''; } $html .= '
' . esc_html__( 'Guest', 'wp-bnb' ) . '' . esc_html__( 'Bookings', 'wp-bnb' ) . '' . esc_html__( 'Nights', 'wp-bnb' ) . '' . esc_html__( 'Spent', 'wp-bnb' ) . '
' . esc_html( $guest['name'] ) . '' . $guest['bookings'] . '' . $guest['nights'] . '' . esc_html( Calculator::formatPrice( $guest['total_spent'] ) ) . '
'; } 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 ), ); } }