_x( 'Guests', 'post type general name', 'wp-bnb' ), 'singular_name' => _x( 'Guest', 'post type singular name', 'wp-bnb' ), 'menu_name' => _x( 'Guests', 'admin menu', 'wp-bnb' ), 'name_admin_bar' => _x( 'Guest', 'add new on admin bar', 'wp-bnb' ), 'add_new' => _x( 'Add New', 'guest', 'wp-bnb' ), 'add_new_item' => __( 'Add New Guest', 'wp-bnb' ), 'new_item' => __( 'New Guest', 'wp-bnb' ), 'edit_item' => __( 'Edit Guest', 'wp-bnb' ), 'view_item' => __( 'View Guest', 'wp-bnb' ), 'all_items' => __( 'Guests', 'wp-bnb' ), 'search_items' => __( 'Search Guests', 'wp-bnb' ), 'parent_item_colon' => __( 'Parent Guests:', 'wp-bnb' ), 'not_found' => __( 'No guests found.', 'wp-bnb' ), 'not_found_in_trash' => __( 'No guests found in Trash.', 'wp-bnb' ), 'archives' => __( 'Guest archives', 'wp-bnb' ), 'insert_into_item' => __( 'Insert into guest', 'wp-bnb' ), 'uploaded_to_this_item' => __( 'Uploaded to this guest', 'wp-bnb' ), 'filter_items_list' => __( 'Filter guests list', 'wp-bnb' ), 'items_list_navigation' => __( 'Guests list navigation', 'wp-bnb' ), 'items_list' => __( 'Guests list', 'wp-bnb' ), ); $args = array( 'labels' => $labels, 'public' => false, 'publicly_queryable' => false, 'show_ui' => true, 'show_in_menu' => 'wp-bnb', 'query_var' => false, 'rewrite' => false, 'capability_type' => 'post', 'has_archive' => false, 'hierarchical' => false, 'menu_position' => null, 'menu_icon' => 'dashicons-groups', 'supports' => array( 'title', 'revisions', ), 'show_in_rest' => true, 'rest_base' => 'guests', 'rest_controller_class' => 'WP_REST_Posts_Controller', ); register_post_type( self::POST_TYPE, $args ); } /** * Add meta boxes. * * @return void */ public static function add_meta_boxes(): void { add_meta_box( 'bnb_guest_personal', __( 'Personal Information', 'wp-bnb' ), array( self::class, 'render_personal_meta_box' ), self::POST_TYPE, 'normal', 'high' ); add_meta_box( 'bnb_guest_address', __( 'Address', 'wp-bnb' ), array( self::class, 'render_address_meta_box' ), self::POST_TYPE, 'normal', 'high' ); add_meta_box( 'bnb_guest_identification', __( 'Identification', 'wp-bnb' ), array( self::class, 'render_identification_meta_box' ), self::POST_TYPE, 'normal', 'default' ); add_meta_box( 'bnb_guest_consent', __( 'Consent & Privacy', 'wp-bnb' ), array( self::class, 'render_consent_meta_box' ), self::POST_TYPE, 'side', 'high' ); add_meta_box( 'bnb_guest_bookings', __( 'Booking History', 'wp-bnb' ), array( self::class, 'render_bookings_meta_box' ), self::POST_TYPE, 'normal', 'default' ); add_meta_box( 'bnb_guest_status', __( 'Status & Notes', 'wp-bnb' ), array( self::class, 'render_status_meta_box' ), self::POST_TYPE, 'side', 'default' ); } /** * Render personal information meta box. * * @param \WP_Post $post Current post object. * @return void */ public static function render_personal_meta_box( \WP_Post $post ): void { wp_nonce_field( 'bnb_guest_meta', 'bnb_guest_meta_nonce' ); $first_name = get_post_meta( $post->ID, self::META_PREFIX . 'first_name', true ); $last_name = get_post_meta( $post->ID, self::META_PREFIX . 'last_name', true ); $email = get_post_meta( $post->ID, self::META_PREFIX . 'email', true ); $phone = get_post_meta( $post->ID, self::META_PREFIX . 'phone', true ); $date_of_birth = get_post_meta( $post->ID, self::META_PREFIX . 'date_of_birth', true ); $nationality = get_post_meta( $post->ID, self::META_PREFIX . 'nationality', true ); ?>

ID, self::META_PREFIX . 'street', true ); $city = get_post_meta( $post->ID, self::META_PREFIX . 'city', true ); $postal_code = get_post_meta( $post->ID, self::META_PREFIX . 'postal_code', true ); $country = get_post_meta( $post->ID, self::META_PREFIX . 'country', true ); ?>
ID, self::META_PREFIX . 'id_type', true ); $id_number = get_post_meta( $post->ID, self::META_PREFIX . 'id_number', true ); $id_expiry = get_post_meta( $post->ID, self::META_PREFIX . 'id_expiry', true ); ?>

ID, self::META_PREFIX . 'consent_marketing', true ); $consent_data = get_post_meta( $post->ID, self::META_PREFIX . 'consent_data', true ); $consent_date = get_post_meta( $post->ID, self::META_PREFIX . 'consent_date', true ); ?>


ID ); $total_spent = self::get_total_spent( $post->ID ); $booking_count = count( $bookings ); if ( 0 === $booking_count ) { echo '

' . esc_html__( 'No bookings found for this guest.', 'wp-bnb' ) . '

'; return; } ?>
ID, '_bnb_booking_reference', true ); $room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true ); $room = get_post( $room_id ); $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 = get_post_meta( $booking->ID, '_bnb_booking_total_price', true ); $status = get_post_meta( $booking->ID, '_bnb_booking_status', true ); $statuses = Booking::get_statuses(); $colors = Booking::get_status_colors(); ?>
ID ); ?> post_title : '—' ); ?>
10 ) : ?>

ID, self::META_PREFIX . 'status', true ) ?: 'active'; $notes = get_post_meta( $post->ID, self::META_PREFIX . 'notes', true ); $preferences = get_post_meta( $post->ID, self::META_PREFIX . 'preferences', true ); ?>

post_title !== $full_name ) { remove_action( 'save_post_' . self::POST_TYPE, array( self::class, 'save_meta' ), 10 ); wp_update_post( array( 'ID' => $post_id, 'post_title' => $full_name, ) ); add_action( 'save_post_' . self::POST_TYPE, array( self::class, 'save_meta' ), 10, 2 ); } } } /** * Add custom columns to the post list. * * @param array $columns Existing columns. * @return array */ public static function add_columns( array $columns ): array { $new_columns = array(); foreach ( $columns as $key => $value ) { $new_columns[ $key ] = $value; if ( 'title' === $key ) { $new_columns['email'] = __( 'Email', 'wp-bnb' ); $new_columns['phone'] = __( 'Phone', 'wp-bnb' ); $new_columns['country'] = __( 'Country', 'wp-bnb' ); $new_columns['bookings'] = __( 'Bookings', 'wp-bnb' ); $new_columns['status'] = __( 'Status', 'wp-bnb' ); } } // Remove default date column. unset( $new_columns['date'] ); return $new_columns; } /** * Render custom column content. * * @param string $column Column name. * @param int $post_id Post ID. * @return void */ public static function render_column( string $column, int $post_id ): void { switch ( $column ) { case 'email': $email = get_post_meta( $post_id, self::META_PREFIX . 'email', true ); if ( $email ) { printf( '%s', esc_attr( $email ), esc_html( $email ) ); } else { echo '—'; } break; case 'phone': $phone = get_post_meta( $post_id, self::META_PREFIX . 'phone', true ); echo esc_html( $phone ?: '—' ); break; case 'country': $country = get_post_meta( $post_id, self::META_PREFIX . 'country', true ); if ( $country ) { $countries = Building::get_countries(); echo esc_html( $countries[ $country ] ?? $country ); } else { echo '—'; } break; case 'bookings': $count = self::get_booking_count( $post_id ); if ( $count > 0 ) { printf( '%s', esc_url( admin_url( 'edit.php?post_type=' . Booking::POST_TYPE . '&guest_id=' . $post_id ) ), esc_html( sprintf( /* translators: %d: Number of bookings */ _n( '%d booking', '%d bookings', $count, 'wp-bnb' ), $count ) ) ); } else { echo '—'; } break; case 'status': $status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'active'; $colors = self::get_status_colors(); $statuses = self::get_statuses(); printf( '%s', esc_attr( $colors[ $status ] ?? '#999' ), esc_html( $statuses[ $status ] ?? $status ) ); break; } } /** * Add sortable columns. * * @param array $columns Existing sortable columns. * @return array */ public static function sortable_columns( array $columns ): array { $columns['email'] = 'email'; $columns['country'] = 'country'; $columns['status'] = 'status'; return $columns; } /** * Add filter dropdowns to admin list. * * @param string $post_type Current post type. * @return void */ public static function add_filters( string $post_type ): void { if ( self::POST_TYPE !== $post_type ) { return; } // Status filter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only. $selected_status = isset( $_GET['guest_status'] ) ? sanitize_text_field( wp_unslash( $_GET['guest_status'] ) ) : ''; ?> is_main_query() ) { return; } if ( self::POST_TYPE !== $query->get( 'post_type' ) ) { return; } $meta_query = array(); // Status filter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only. if ( ! empty( $_GET['guest_status'] ) ) { $meta_query[] = array( 'key' => self::META_PREFIX . 'status', 'value' => sanitize_text_field( wp_unslash( $_GET['guest_status'] ) ), ); } // Country filter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only. if ( ! empty( $_GET['guest_country'] ) ) { $meta_query[] = array( 'key' => self::META_PREFIX . 'country', 'value' => sanitize_text_field( wp_unslash( $_GET['guest_country'] ) ), ); } if ( ! empty( $meta_query ) ) { $meta_query['relation'] = 'AND'; $query->set( 'meta_query', $meta_query ); } // Handle orderby for custom columns. $orderby = $query->get( 'orderby' ); if ( 'email' === $orderby ) { $query->set( 'meta_key', self::META_PREFIX . 'email' ); $query->set( 'orderby', 'meta_value' ); } elseif ( 'country' === $orderby ) { $query->set( 'meta_key', self::META_PREFIX . 'country' ); $query->set( 'orderby', 'meta_value' ); } elseif ( 'status' === $orderby ) { $query->set( 'meta_key', self::META_PREFIX . 'status' ); $query->set( 'orderby', 'meta_value' ); } } /** * Change title placeholder. * * @param string $placeholder Default placeholder. * @param \WP_Post $post Current post. * @return string */ public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string { if ( self::POST_TYPE === $post->post_type ) { return __( 'Guest name (auto-generated from first/last name)', 'wp-bnb' ); } return $placeholder; } /** * Get guest statuses. * * @return array */ public static function get_statuses(): array { return array( 'active' => __( 'Active', 'wp-bnb' ), 'inactive' => __( 'Inactive', 'wp-bnb' ), 'blocked' => __( 'Blocked', 'wp-bnb' ), ); } /** * Get status colors. * * @return array */ public static function get_status_colors(): array { return array( 'active' => '#00a32a', 'inactive' => '#72aee6', 'blocked' => '#d63638', ); } /** * Get ID types. * * @return array */ public static function get_id_types(): array { return array( 'passport' => __( 'Passport', 'wp-bnb' ), 'id_card' => __( 'ID Card', 'wp-bnb' ), 'drivers_license' => __( "Driver's License", 'wp-bnb' ), 'other' => __( 'Other', 'wp-bnb' ), ); } /** * Get guest by email. * * @param string $email Email address to search for. * @return \WP_Post|null Guest post or null if not found. */ public static function get_by_email( string $email ): ?\WP_Post { if ( empty( $email ) ) { return null; } $guests = get_posts( array( 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 1, 'meta_query' => array( array( 'key' => self::META_PREFIX . 'email', 'value' => $email, ), ), ) ); return ! empty( $guests ) ? $guests[0] : null; } /** * Get all bookings for a guest. * * @param int $guest_id Guest post ID. * @return array Array of booking posts. */ public static function get_bookings( int $guest_id ): array { $email = get_post_meta( $guest_id, self::META_PREFIX . 'email', true ); if ( empty( $email ) ) { return array(); } // First try to find bookings by guest_id (new way). $bookings_by_id = get_posts( array( 'post_type' => Booking::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => '_bnb_booking_guest_id', 'value' => $guest_id, ), ), ) ); // Also find bookings by email (legacy way). $bookings_by_email = get_posts( array( 'post_type' => Booking::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => '_bnb_booking_guest_email', 'value' => $email, ), ), ) ); // Merge and deduplicate. $all_bookings = array_merge( $bookings_by_id, $bookings_by_email ); $unique_ids = array(); $result = array(); foreach ( $all_bookings as $booking ) { if ( ! in_array( $booking->ID, $unique_ids, true ) ) { $unique_ids[] = $booking->ID; $result[] = $booking; } } // Sort by check-in date descending. usort( $result, function ( $a, $b ) { $date_a = get_post_meta( $a->ID, '_bnb_booking_check_in', true ); $date_b = get_post_meta( $b->ID, '_bnb_booking_check_in', true ); return strcmp( $date_b, $date_a ); } ); return $result; } /** * Get booking count for a guest. * * @param int $guest_id Guest post ID. * @return int Number of bookings. */ public static function get_booking_count( int $guest_id ): int { return count( self::get_bookings( $guest_id ) ); } /** * Get total spent by a guest. * * @param int $guest_id Guest post ID. * @return float Total amount spent. */ public static function get_total_spent( int $guest_id ): float { $bookings = self::get_bookings( $guest_id ); $total = 0.0; foreach ( $bookings as $booking ) { $status = get_post_meta( $booking->ID, '_bnb_booking_status', true ); // Only count completed bookings (checked_out) or confirmed ones. if ( in_array( $status, array( 'confirmed', 'checked_in', 'checked_out' ), true ) ) { $price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true ); $total += floatval( $price ); } } return $total; } /** * Get formatted address for a guest. * * @param int $guest_id Guest post ID. * @return string Formatted address. */ public static function get_formatted_address( int $guest_id ): string { $street = get_post_meta( $guest_id, self::META_PREFIX . 'street', true ); $city = get_post_meta( $guest_id, self::META_PREFIX . 'city', true ); $postal_code = get_post_meta( $guest_id, self::META_PREFIX . 'postal_code', true ); $country = get_post_meta( $guest_id, self::META_PREFIX . 'country', true ); $parts = array(); if ( $street ) { $parts[] = $street; } if ( $postal_code || $city ) { $parts[] = trim( $postal_code . ' ' . $city ); } if ( $country ) { $countries = Building::get_countries(); $parts[] = $countries[ $country ] ?? $country; } return implode( "\n", $parts ); } /** * Get guest's full name. * * @param int $guest_id Guest post ID. * @return string Full name. */ public static function get_full_name( int $guest_id ): string { $first_name = get_post_meta( $guest_id, self::META_PREFIX . 'first_name', true ); $last_name = get_post_meta( $guest_id, self::META_PREFIX . 'last_name', true ); return trim( $first_name . ' ' . $last_name ); } }