'', 'check_out' => '', 'guests' => 0, 'room_type' => '', 'amenities' => array(), 'price_min' => 0, 'price_max' => 0, 'building_id' => 0, 'orderby' => 'title', 'order' => 'ASC', 'limit' => -1, 'offset' => 0, ); $args = wp_parse_args( $args, $defaults ); // Build base query. $query_args = array( 'post_type' => Room::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => (int) $args['limit'], 'offset' => (int) $args['offset'], 'meta_query' => array( 'relation' => 'AND', ), 'tax_query' => array( 'relation' => 'AND', ), ); // Filter by building. if ( ! empty( $args['building_id'] ) ) { $query_args['meta_query'][] = array( 'key' => '_bnb_room_building_id', 'value' => (int) $args['building_id'], ); } // Filter by capacity. if ( ! empty( $args['guests'] ) && (int) $args['guests'] > 0 ) { $query_args['meta_query'][] = array( 'key' => '_bnb_room_capacity', 'value' => (int) $args['guests'], 'compare' => '>=', 'type' => 'NUMERIC', ); } // Filter by room status (only available rooms). $query_args['meta_query'][] = array( 'relation' => 'OR', array( 'key' => '_bnb_room_status', 'value' => 'available', ), array( 'key' => '_bnb_room_status', 'compare' => 'NOT EXISTS', ), ); // Filter by room type. if ( ! empty( $args['room_type'] ) ) { $query_args['tax_query'][] = array( 'taxonomy' => RoomType::TAXONOMY, 'field' => is_numeric( $args['room_type'] ) ? 'term_id' : 'slug', 'terms' => $args['room_type'], ); } // Filter by amenities (all must match). if ( ! empty( $args['amenities'] ) ) { $amenities = is_array( $args['amenities'] ) ? $args['amenities'] : explode( ',', $args['amenities'] ); $amenities = array_map( 'trim', $amenities ); $amenities = array_filter( $amenities ); if ( ! empty( $amenities ) ) { $query_args['tax_query'][] = array( 'taxonomy' => Amenity::TAXONOMY, 'field' => is_numeric( $amenities[0] ) ? 'term_id' : 'slug', 'terms' => $amenities, 'operator' => 'AND', ); } } // Handle ordering. switch ( $args['orderby'] ) { case 'price': $query_args['meta_key'] = '_bnb_room_price_' . PricingTier::SHORT_TERM->value; $query_args['orderby'] = 'meta_value_num'; break; case 'capacity': $query_args['meta_key'] = '_bnb_room_capacity'; $query_args['orderby'] = 'meta_value_num'; break; case 'date': $query_args['orderby'] = 'date'; break; case 'random': $query_args['orderby'] = 'rand'; break; default: $query_args['orderby'] = 'title'; break; } $query_args['order'] = strtoupper( $args['order'] ) === 'DESC' ? 'DESC' : 'ASC'; // Execute query. $rooms = get_posts( $query_args ); // Filter by availability if dates provided. if ( ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) { $rooms = self::filter_by_availability( $rooms, $args['check_in'], $args['check_out'] ); } // Filter by price range. if ( ( ! empty( $args['price_min'] ) || ! empty( $args['price_max'] ) ) && ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) { $rooms = self::filter_by_price_range( $rooms, (float) $args['price_min'], (float) $args['price_max'], $args['check_in'], $args['check_out'] ); } // Build result array with room data. $results = array(); foreach ( $rooms as $room ) { $results[] = self::get_room_data( $room, $args['check_in'], $args['check_out'] ); } return $results; } /** * Filter rooms by availability. * * @param array $rooms Array of WP_Post objects. * @param string $check_in Check-in date (Y-m-d). * @param string $check_out Check-out date (Y-m-d). * @return array Filtered rooms. */ public static function filter_by_availability( array $rooms, string $check_in, string $check_out ): array { return array_filter( $rooms, function ( $room ) use ( $check_in, $check_out ) { return Availability::is_available( $room->ID, $check_in, $check_out ); } ); } /** * Filter rooms by price range. * * @param array $rooms Array of WP_Post objects. * @param float $min Minimum price. * @param float $max Maximum price. * @param string $check_in Check-in date. * @param string $check_out Check-out date. * @return array Filtered rooms. */ public static function filter_by_price_range( array $rooms, float $min, float $max, string $check_in, string $check_out ): array { return array_filter( $rooms, function ( $room ) use ( $min, $max, $check_in, $check_out ) { try { $calculator = new Calculator( $room->ID, $check_in, $check_out ); $price = $calculator->calculate(); if ( $min > 0 && $price < $min ) { return false; } if ( $max > 0 && $price > $max ) { return false; } return true; } catch ( \Exception $e ) { return false; } } ); } /** * Get complete room data for display. * * @param \WP_Post $room Room post object. * @param string $check_in Optional check-in date. * @param string $check_out Optional check-out date. * @return array Room data array. */ public static function get_room_data( \WP_Post $room, string $check_in = '', string $check_out = '' ): array { $building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true ); $building = $building_id ? get_post( $building_id ) : null; // Get room types. $room_types = wp_get_post_terms( $room->ID, RoomType::TAXONOMY, array( 'fields' => 'names' ) ); // Get amenities with icons. $amenities = wp_get_post_terms( $room->ID, Amenity::TAXONOMY ); $amenity_list = array(); foreach ( $amenities as $amenity ) { $amenity_list[] = array( 'id' => $amenity->term_id, 'name' => $amenity->name, 'slug' => $amenity->slug, 'icon' => get_term_meta( $amenity->term_id, 'amenity_icon', true ), ); } // Get gallery images. $gallery_ids = get_post_meta( $room->ID, '_bnb_room_gallery', true ); $gallery = array(); if ( $gallery_ids ) { $ids = explode( ',', $gallery_ids ); foreach ( $ids as $id ) { $image = wp_get_attachment_image_src( (int) $id, 'large' ); if ( $image ) { $gallery[] = array( 'id' => (int) $id, 'url' => $image[0], 'width' => $image[1], 'height' => $image[2], 'thumb' => wp_get_attachment_image_src( (int) $id, 'thumbnail' )[0] ?? $image[0], ); } } } // Get pricing. $pricing = Calculator::getRoomPricing( $room->ID ); $nightly_price = $pricing[ PricingTier::SHORT_TERM->value ]['price'] ?? null; // Calculate stay price if dates provided. $stay_price = null; $nights = 0; if ( ! empty( $check_in ) && ! empty( $check_out ) ) { try { $calculator = new Calculator( $room->ID, $check_in, $check_out ); $stay_price = $calculator->calculate(); $nights = $calculator->getNights(); } catch ( \Exception $e ) { $stay_price = null; } } return array( 'id' => $room->ID, 'title' => $room->post_title, 'slug' => $room->post_name, 'excerpt' => get_the_excerpt( $room ), 'content' => apply_filters( 'the_content', $room->post_content ), 'permalink' => get_permalink( $room->ID ), 'featured_image' => get_the_post_thumbnail_url( $room->ID, 'large' ), 'thumbnail' => get_the_post_thumbnail_url( $room->ID, 'medium' ), 'gallery' => $gallery, 'building' => $building ? array( 'id' => $building->ID, 'title' => $building->post_title, 'permalink' => get_permalink( $building->ID ), 'city' => get_post_meta( $building->ID, '_bnb_building_city', true ), ) : null, 'room_number' => get_post_meta( $room->ID, '_bnb_room_room_number', true ), 'floor' => (int) get_post_meta( $room->ID, '_bnb_room_floor', true ), 'capacity' => (int) get_post_meta( $room->ID, '_bnb_room_capacity', true ), 'max_adults' => (int) get_post_meta( $room->ID, '_bnb_room_max_adults', true ), 'max_children' => (int) get_post_meta( $room->ID, '_bnb_room_max_children', true ), 'size' => (float) get_post_meta( $room->ID, '_bnb_room_size', true ), 'beds' => get_post_meta( $room->ID, '_bnb_room_beds', true ), 'bathrooms' => (float) get_post_meta( $room->ID, '_bnb_room_bathrooms', true ), 'room_types' => $room_types, 'amenities' => $amenity_list, 'nightly_price' => $nightly_price, 'price_formatted' => $nightly_price ? Calculator::formatPrice( $nightly_price ) : null, 'stay_price' => $stay_price, 'stay_price_formatted' => $stay_price ? Calculator::formatPrice( $stay_price ) : null, 'nights' => $nights, ); } /** * Get data for search form (room types, amenities, buildings). * * @return array Form data. */ public static function get_search_form_data(): array { // Get all room types. $room_types = get_terms( array( 'taxonomy' => RoomType::TAXONOMY, 'hide_empty' => true, 'orderby' => 'meta_value_num', 'meta_key' => 'room_type_sort_order', 'order' => 'ASC', ) ); // Get all amenities. $amenities = get_terms( array( 'taxonomy' => Amenity::TAXONOMY, 'hide_empty' => true, 'orderby' => 'name', 'order' => 'ASC', ) ); // Get all buildings with rooms. $buildings = get_posts( array( 'post_type' => Building::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', ) ); // Filter buildings to only those with rooms. $buildings_with_rooms = array(); foreach ( $buildings as $building ) { $rooms = Room::get_rooms_for_building( $building->ID ); if ( ! empty( $rooms ) ) { $buildings_with_rooms[] = array( 'id' => $building->ID, 'title' => $building->post_title, 'city' => get_post_meta( $building->ID, '_bnb_building_city', true ), ); } } // Get price range from all rooms. $price_range = self::get_price_range(); // Get capacity range. $capacity_range = self::get_capacity_range(); return array( 'room_types' => array_map( function ( $term ) { return array( 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'parent' => $term->parent, 'count' => $term->count, 'capacity' => (int) get_term_meta( $term->term_id, 'room_type_base_capacity', true ), ); }, $room_types ), 'amenities' => array_map( function ( $term ) { return array( 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'icon' => get_term_meta( $term->term_id, 'amenity_icon', true ), 'count' => $term->count, ); }, $amenities ), 'buildings' => $buildings_with_rooms, 'price_range' => $price_range, 'capacity_range' => $capacity_range, 'currency' => get_option( 'wp_bnb_currency', 'CHF' ), ); } /** * Get price range from all rooms. * * @return array Min and max prices. */ public static function get_price_range(): array { global $wpdb; $meta_key = '_bnb_room_price_' . PricingTier::SHORT_TERM->value; $result = $wpdb->get_row( $wpdb->prepare( "SELECT MIN(CAST(meta_value AS DECIMAL(10,2))) as min_price, MAX(CAST(meta_value AS DECIMAL(10,2))) as max_price FROM {$wpdb->postmeta} pm JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE pm.meta_key = %s AND pm.meta_value != '' AND pm.meta_value > 0 AND p.post_type = %s AND p.post_status = 'publish'", $meta_key, Room::POST_TYPE ) ); return array( 'min' => $result ? (float) $result->min_price : 0, 'max' => $result ? (float) $result->max_price : 500, ); } /** * Get capacity range from all rooms. * * @return array Min and max capacity. */ public static function get_capacity_range(): array { global $wpdb; $result = $wpdb->get_row( $wpdb->prepare( "SELECT MIN(CAST(meta_value AS UNSIGNED)) as min_capacity, MAX(CAST(meta_value AS UNSIGNED)) as max_capacity FROM {$wpdb->postmeta} pm JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE pm.meta_key = '_bnb_room_capacity' AND pm.meta_value != '' AND p.post_type = %s AND p.post_status = 'publish'", Room::POST_TYPE ) ); return array( 'min' => $result && $result->min_capacity ? (int) $result->min_capacity : 1, 'max' => $result && $result->max_capacity ? (int) $result->max_capacity : 10, ); } /** * AJAX handler for room search. * * @return void */ public static function ajax_search_rooms(): void { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API. $args = array( 'check_in' => isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '', 'check_out' => isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '', 'guests' => isset( $_POST['guests'] ) ? absint( $_POST['guests'] ) : 0, 'room_type' => isset( $_POST['room_type'] ) ? sanitize_text_field( wp_unslash( $_POST['room_type'] ) ) : '', 'amenities' => isset( $_POST['amenities'] ) ? array_map( 'sanitize_text_field', (array) $_POST['amenities'] ) : array(), 'price_min' => isset( $_POST['price_min'] ) ? (float) $_POST['price_min'] : 0, 'price_max' => isset( $_POST['price_max'] ) ? (float) $_POST['price_max'] : 0, 'building_id' => isset( $_POST['building_id'] ) ? absint( $_POST['building_id'] ) : 0, 'orderby' => isset( $_POST['orderby'] ) ? sanitize_text_field( wp_unslash( $_POST['orderby'] ) ) : 'title', 'order' => isset( $_POST['order'] ) ? sanitize_text_field( wp_unslash( $_POST['order'] ) ) : 'ASC', 'limit' => isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 12, 'offset' => isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0, ); // phpcs:enable WordPress.Security.NonceVerification.Missing // Validate dates if provided. if ( ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) { $check_in = strtotime( $args['check_in'] ); $check_out = strtotime( $args['check_out'] ); if ( ! $check_in || ! $check_out || $check_out <= $check_in ) { wp_send_json_error( array( 'message' => __( 'Invalid date range.', 'wp-bnb' ) ) ); } if ( $check_in < strtotime( 'today' ) ) { wp_send_json_error( array( 'message' => __( 'Check-in date cannot be in the past.', 'wp-bnb' ) ) ); } } $results = self::search( $args ); wp_send_json_success( array( 'rooms' => $results, 'count' => count( $results ), 'args' => $args, ) ); } /** * AJAX handler for availability check. * * @return void */ public static function ajax_get_availability(): void { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API. $room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0; $check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : ''; $check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( ! $room_id || ! $check_in || ! $check_out ) { wp_send_json_error( array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) ) ); } $available = Availability::is_available( $room_id, $check_in, $check_out ); $result = array( 'available' => $available, 'room_id' => $room_id, 'check_in' => $check_in, 'check_out' => $check_out, ); if ( $available ) { try { $calculator = new Calculator( $room_id, $check_in, $check_out ); $price = $calculator->calculate(); $result['price'] = $price; $result['price_formatted'] = Calculator::formatPrice( $price ); $result['nights'] = $calculator->getNights(); $result['breakdown'] = $calculator->getBreakdown(); } catch ( \Exception $e ) { $result['price_error'] = $e->getMessage(); } } wp_send_json_success( $result ); } /** * AJAX handler for calendar data. * * @return void */ public static function ajax_get_calendar(): void { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API. $room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0; $year = isset( $_POST['year'] ) ? absint( $_POST['year'] ) : (int) gmdate( 'Y' ); $month = isset( $_POST['month'] ) ? absint( $_POST['month'] ) : (int) gmdate( 'n' ); // phpcs:enable WordPress.Security.NonceVerification.Missing if ( ! $room_id ) { wp_send_json_error( array( 'message' => __( 'Room ID is required.', 'wp-bnb' ) ) ); } // Validate month. $month = max( 1, min( 12, $month ) ); // Get calendar data. $calendar = Availability::get_calendar_data( $room_id, $year, $month ); // Simplify for frontend (remove booking details, just show availability). $days = array(); foreach ( $calendar['days'] as $day_num => $day_data ) { $days[ $day_num ] = array( 'date' => $day_data['date'], 'day' => $day_data['day'], 'available' => ! $day_data['is_booked'], 'is_past' => $day_data['is_past'], 'is_today' => $day_data['is_today'], ); } wp_send_json_success( array( 'room_id' => $room_id, 'year' => $year, 'month' => $month, 'month_name' => $calendar['month_name'], 'days_in_month' => $calendar['days_in_month'], 'first_day_of_week' => $calendar['first_day_of_week'], 'days' => $days, 'prev_month' => $calendar['prev_month'], 'next_month' => $calendar['next_month'], ) ); } /** * AJAX handler for price calculation. * * @return void */ public static function ajax_calculate_price(): void { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API. $room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0; $check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : ''; $check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( ! $room_id || ! $check_in || ! $check_out ) { wp_send_json_error( array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) ) ); } try { $calculator = new Calculator( $room_id, $check_in, $check_out ); $price = $calculator->calculate(); $breakdown = $calculator->getBreakdown(); wp_send_json_success( array( 'room_id' => $room_id, 'check_in' => $check_in, 'check_out' => $check_out, 'nights' => $calculator->getNights(), 'price' => $price, 'price_formatted' => Calculator::formatPrice( $price ), 'tier' => $breakdown['tier'] ?? null, 'breakdown' => $breakdown, ) ); } catch ( \Exception $e ) { wp_send_json_error( array( 'message' => $e->getMessage() ) ); } } }