_x( 'Bookings', 'post type general name', 'wp-bnb' ),
'singular_name' => _x( 'Booking', 'post type singular name', 'wp-bnb' ),
'menu_name' => _x( 'Bookings', 'admin menu', 'wp-bnb' ),
'name_admin_bar' => _x( 'Booking', 'add new on admin bar', 'wp-bnb' ),
'add_new' => _x( 'Add New', 'booking', 'wp-bnb' ),
'add_new_item' => __( 'Add New Booking', 'wp-bnb' ),
'new_item' => __( 'New Booking', 'wp-bnb' ),
'edit_item' => __( 'Edit Booking', 'wp-bnb' ),
'view_item' => __( 'View Booking', 'wp-bnb' ),
'all_items' => __( 'Bookings', 'wp-bnb' ),
'search_items' => __( 'Search Bookings', 'wp-bnb' ),
'parent_item_colon' => __( 'Parent Bookings:', 'wp-bnb' ),
'not_found' => __( 'No bookings found.', 'wp-bnb' ),
'not_found_in_trash' => __( 'No bookings found in Trash.', 'wp-bnb' ),
'archives' => __( 'Booking archives', 'wp-bnb' ),
'insert_into_item' => __( 'Insert into booking', 'wp-bnb' ),
'uploaded_to_this_item' => __( 'Uploaded to this booking', 'wp-bnb' ),
'filter_items_list' => __( 'Filter bookings list', 'wp-bnb' ),
'items_list_navigation' => __( 'Bookings list navigation', 'wp-bnb' ),
'items_list' => __( 'Bookings list', 'wp-bnb' ),
);
$args = array(
'labels' => $labels,
'public' => false,
'publicly_queryable' => false,
'show_ui' => true,
'show_in_menu' => 'wp-bnb',
'query_var' => false,
'capability_type' => 'post',
'has_archive' => false,
'hierarchical' => false,
'menu_position' => null,
'menu_icon' => 'dashicons-calendar-alt',
'supports' => array( 'revisions' ),
'show_in_rest' => true,
'rest_base' => 'bookings',
'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_booking_room_dates',
__( 'Room & Dates', 'wp-bnb' ),
array( self::class, 'render_room_dates_meta_box' ),
self::POST_TYPE,
'normal',
'high'
);
add_meta_box(
'bnb_booking_guest',
__( 'Guest Information', 'wp-bnb' ),
array( self::class, 'render_guest_meta_box' ),
self::POST_TYPE,
'normal',
'high'
);
add_meta_box(
'bnb_booking_services',
__( 'Additional Services', 'wp-bnb' ),
array( self::class, 'render_services_meta_box' ),
self::POST_TYPE,
'normal',
'default'
);
add_meta_box(
'bnb_booking_pricing',
__( 'Pricing', 'wp-bnb' ),
array( self::class, 'render_pricing_meta_box' ),
self::POST_TYPE,
'normal',
'default'
);
add_meta_box(
'bnb_booking_status',
__( 'Status & Notes', 'wp-bnb' ),
array( self::class, 'render_status_meta_box' ),
self::POST_TYPE,
'side',
'high'
);
}
/**
* Render room and dates meta box.
*
* @param \WP_Post $post Current post object.
* @return void
*/
public static function render_room_dates_meta_box( \WP_Post $post ): void {
wp_nonce_field( 'bnb_booking_meta', 'bnb_booking_meta_nonce' );
$room_id = get_post_meta( $post->ID, self::META_PREFIX . 'room_id', true );
$check_in = get_post_meta( $post->ID, self::META_PREFIX . 'check_in', true );
$check_out = get_post_meta( $post->ID, self::META_PREFIX . 'check_out', true );
$rooms = get_posts(
array(
'post_type' => Room::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
)
);
// Group rooms by building.
$rooms_by_building = array();
foreach ( $rooms as $room ) {
$building = Room::get_building( $room->ID );
$building_name = $building ? $building->post_title : __( 'No Building', 'wp-bnb' );
$building_id = $building ? $building->ID : 0;
if ( ! isset( $rooms_by_building[ $building_id ] ) ) {
$rooms_by_building[ $building_id ] = array(
'name' => $building_name,
'rooms' => array(),
);
}
$rooms_by_building[ $building_id ]['rooms'][] = $room;
}
?>
ID, self::META_PREFIX . 'guest_id', true );
$guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true );
$guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true );
$guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true );
$adults = get_post_meta( $post->ID, self::META_PREFIX . 'adults', true );
$children = get_post_meta( $post->ID, self::META_PREFIX . 'children', true );
$guest_notes = get_post_meta( $post->ID, self::META_PREFIX . 'guest_notes', true );
// If guest_id exists, get guest data from Guest CPT.
$linked_guest = null;
if ( $guest_id ) {
$linked_guest = get_post( $guest_id );
if ( $linked_guest && Guest::POST_TYPE === $linked_guest->post_type ) {
$guest_name = Guest::get_full_name( $guest_id );
$guest_email = get_post_meta( $guest_id, '_bnb_guest_email', true );
$guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true );
} else {
$linked_guest = null;
$guest_id = '';
}
}
?>
ID, self::SERVICES_META_KEY, true ) ?: array();
$check_in = get_post_meta( $post->ID, self::META_PREFIX . 'check_in', true );
$check_out = get_post_meta( $post->ID, self::META_PREFIX . 'check_out', true );
$nights = ( $check_in && $check_out ) ? self::calculate_nights( $check_in, $check_out ) : 1;
// Get all active services.
$available_services = Service::get_services_for_booking();
if ( empty( $available_services ) ) {
?>
ID );
echo esc_html( Calculator::formatPrice( $services_total ) );
?>
ID, self::META_PREFIX . 'calculated_price', true );
$price_breakdown = get_post_meta( $post->ID, self::META_PREFIX . 'price_breakdown', true );
$override_price = get_post_meta( $post->ID, self::META_PREFIX . 'override_price', true );
$currency = get_option( 'wp_bnb_currency', 'CHF' );
?>
ID, self::META_PREFIX . 'status', true ) ?: 'pending';
$notes = get_post_meta( $post->ID, self::META_PREFIX . 'notes', true );
$confirmed_at = get_post_meta( $post->ID, self::META_PREFIX . 'confirmed_at', true );
$statuses = self::get_booking_statuses();
$colors = self::get_status_colors();
?>
post_type ) {
update_post_meta( $post_id, self::META_PREFIX . 'guest_id', $guest_id );
// Sync guest data from Guest CPT for searching/display purposes.
update_post_meta( $post_id, self::META_PREFIX . 'guest_name', Guest::get_full_name( $guest_id ) );
update_post_meta( $post_id, self::META_PREFIX . 'guest_email', get_post_meta( $guest_id, '_bnb_guest_email', true ) );
update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', get_post_meta( $guest_id, '_bnb_guest_phone', true ) );
} else {
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
}
} else {
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
// Guest text fields (only save if no guest_id).
$guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' );
foreach ( $guest_fields as $field ) {
$key = 'bnb_booking_' . $field;
if ( isset( $_POST[ $key ] ) ) {
$value = wp_unslash( $_POST[ $key ] );
if ( 'guest_email' === $field ) {
$value = sanitize_email( $value );
} elseif ( 'guest_notes' === $field ) {
$value = sanitize_textarea_field( $value );
} else {
$value = sanitize_text_field( $value );
}
update_post_meta( $post_id, self::META_PREFIX . $field, $value );
}
}
}
// Guest notes are always saved (per-booking notes).
if ( isset( $_POST['bnb_booking_guest_notes'] ) ) {
update_post_meta(
$post_id,
self::META_PREFIX . 'guest_notes',
sanitize_textarea_field( wp_unslash( $_POST['bnb_booking_guest_notes'] ) )
);
}
// Guest counts.
if ( isset( $_POST['bnb_booking_adults'] ) ) {
update_post_meta( $post_id, self::META_PREFIX . 'adults', absint( $_POST['bnb_booking_adults'] ) );
}
if ( isset( $_POST['bnb_booking_children'] ) ) {
update_post_meta( $post_id, self::META_PREFIX . 'children', absint( $_POST['bnb_booking_children'] ) );
}
// Notes.
if ( isset( $_POST['bnb_booking_notes'] ) ) {
update_post_meta(
$post_id,
self::META_PREFIX . 'notes',
sanitize_textarea_field( wp_unslash( $_POST['bnb_booking_notes'] ) )
);
}
// Pricing.
$override_price = isset( $_POST['bnb_booking_override_price'] ) && '' !== $_POST['bnb_booking_override_price']
? floatval( $_POST['bnb_booking_override_price'] )
: null;
if ( null !== $override_price ) {
update_post_meta( $post_id, self::META_PREFIX . 'override_price', $override_price );
update_post_meta( $post_id, self::META_PREFIX . 'calculated_price', $override_price );
} else {
delete_post_meta( $post_id, self::META_PREFIX . 'override_price' );
// Calculate price if room and dates are set.
if ( $room_id && $check_in && $check_out ) {
try {
$calculator = new Calculator( $room_id, $check_in, $check_out );
$price = $calculator->calculate();
$breakdown = $calculator->getBreakdown();
update_post_meta( $post_id, self::META_PREFIX . 'calculated_price', $price );
update_post_meta( $post_id, self::META_PREFIX . 'price_breakdown', $breakdown );
} catch ( \Exception $e ) {
// Keep existing price if calculation fails.
}
}
}
// Services.
$services_data = array();
if ( isset( $_POST['bnb_booking_services'] ) && is_array( $_POST['bnb_booking_services'] ) ) {
$nights = ( $check_in && $check_out ) ? self::calculate_nights( $check_in, $check_out ) : 1;
foreach ( $_POST['bnb_booking_services'] as $service_id => $service_input ) {
$service_id = absint( $service_id );
if ( ! $service_id ) {
continue;
}
// Only include if selected checkbox is checked.
if ( empty( $service_input['selected'] ) ) {
continue;
}
// Verify service exists and is active.
$service_data = Service::get_service_data( $service_id );
if ( ! $service_data || 'active' !== $service_data['status'] ) {
continue;
}
$quantity = isset( $service_input['quantity'] ) ? absint( $service_input['quantity'] ) : 1;
$quantity = max( 1, min( $quantity, $service_data['max_quantity'] ) );
$price = Service::calculate_service_price( $service_id, $quantity, $nights );
$services_data[] = array(
'service_id' => $service_id,
'quantity' => $quantity,
'price' => $price,
'pricing_type' => $service_data['pricing_type'],
);
}
}
update_post_meta( $post_id, self::SERVICES_META_KEY, $services_data );
// Trigger status change action.
if ( $old_status && $status !== $old_status ) {
/**
* Fires when a booking status changes.
*
* @param int $post_id Booking post ID.
* @param string $old_status Previous status.
* @param string $status New status.
*/
do_action( 'wp_bnb_booking_status_changed', $post_id, $old_status, $status );
}
}
/**
* 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['room'] = __( 'Room', 'wp-bnb' );
$new_columns['guest'] = __( 'Guest', 'wp-bnb' );
$new_columns['dates'] = __( 'Dates', 'wp-bnb' );
$new_columns['nights'] = __( 'Nights', 'wp-bnb' );
$new_columns['price'] = __( 'Price', 'wp-bnb' );
$new_columns['status'] = __( 'Status', 'wp-bnb' );
}
}
// Remove date column, we have our own dates.
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 'room':
$room_id = get_post_meta( $post_id, self::META_PREFIX . 'room_id', true );
if ( $room_id ) {
$room = get_post( $room_id );
if ( $room ) {
$building = Room::get_building( $room_id );
printf(
'%s',
esc_url( get_edit_post_link( $room_id ) ),
esc_html( $room->post_title )
);
if ( $building ) {
echo '
' . esc_html( $building->post_title ) . '';
}
} else {
echo '' . esc_html__( 'Room deleted', 'wp-bnb' ) . '';
}
} else {
echo '—';
}
break;
case 'guest':
$guest_id = get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true );
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
$guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true );
if ( $guest_name ) {
if ( $guest_id ) {
// Linked guest - show link to guest profile.
printf(
'%s',
esc_url( get_edit_post_link( $guest_id ) ),
esc_html( $guest_name )
);
echo ' ';
} else {
echo esc_html( $guest_name );
}
if ( $guest_email ) {
echo '
' . esc_html( $guest_email ) . '';
}
} else {
echo '—';
}
break;
case 'dates':
$check_in = get_post_meta( $post_id, self::META_PREFIX . 'check_in', true );
$check_out = get_post_meta( $post_id, self::META_PREFIX . 'check_out', true );
if ( $check_in && $check_out ) {
$format = get_option( 'date_format' );
echo esc_html( wp_date( $format, strtotime( $check_in ) ) );
echo '
' . esc_html__( 'to', 'wp-bnb' ) . ' ' . esc_html( wp_date( $format, strtotime( $check_out ) ) ) . '';
} else {
echo '—';
}
break;
case 'nights':
$check_in = get_post_meta( $post_id, self::META_PREFIX . 'check_in', true );
$check_out = get_post_meta( $post_id, self::META_PREFIX . 'check_out', true );
if ( $check_in && $check_out ) {
$nights = self::calculate_nights( $check_in, $check_out );
echo esc_html( $nights );
} else {
echo '—';
}
break;
case 'price':
$room_price = (float) get_post_meta( $post_id, self::META_PREFIX . 'calculated_price', true );
$services_total = self::calculate_booking_services_total( $post_id );
$total_price = $room_price + $services_total;
if ( $total_price > 0 ) {
echo esc_html( Calculator::formatPrice( $total_price ) );
$override = get_post_meta( $post_id, self::META_PREFIX . 'override_price', true );
if ( $override ) {
echo ' *';
}
if ( $services_total > 0 ) {
echo '
' . esc_html__( 'incl. services', 'wp-bnb' ) . '';
}
} elseif ( $room_price > 0 ) {
echo esc_html( Calculator::formatPrice( $room_price ) );
} else {
echo '—';
}
break;
case 'status':
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'pending';
$statuses = self::get_booking_statuses();
$colors = self::get_status_colors();
?>
Room::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
)
);
if ( ! empty( $rooms ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
$selected_room = isset( $_GET['room_id'] ) ? absint( $_GET['room_id'] ) : 0;
?>
is_main_query() ) {
return;
}
if ( self::POST_TYPE !== $query->get( 'post_type' ) ) {
return;
}
$meta_query = array();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
if ( ! empty( $_GET['room_id'] ) ) {
$meta_query[] = array(
'key' => self::META_PREFIX . 'room_id',
'value' => absint( $_GET['room_id'] ),
);
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
if ( ! empty( $_GET['booking_status'] ) ) {
$meta_query[] = array(
'key' => self::META_PREFIX . 'status',
'value' => sanitize_text_field( wp_unslash( $_GET['booking_status'] ) ),
);
}
if ( ! empty( $meta_query ) ) {
$meta_query['relation'] = 'AND';
$query->set( 'meta_query', $meta_query );
}
// Handle sorting.
$orderby = $query->get( 'orderby' );
if ( 'check_in' === $orderby ) {
$query->set( 'meta_key', self::META_PREFIX . 'check_in' );
$query->set( 'orderby', 'meta_value' );
} elseif ( 'guest_name' === $orderby ) {
$query->set( 'meta_key', self::META_PREFIX . 'guest_name' );
$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 __( 'Booking reference (auto-generated)', 'wp-bnb' );
}
return $placeholder;
}
/**
* Auto-generate booking reference as title.
*
* @param array $data Post data.
* @param array $postarr Post array.
* @return array
*/
public static function auto_generate_title( array $data, array $postarr ): array {
if ( self::POST_TYPE !== $data['post_type'] ) {
return $data;
}
// Only generate if title is empty or matches auto-generated pattern.
if ( empty( $data['post_title'] ) || preg_match( '/^BNB-\d{4}-\d{5}$/', $data['post_title'] ) ) {
$data['post_title'] = self::generate_reference();
}
return $data;
}
/**
* Show conflict notice in admin.
*
* @return void
*/
public static function show_conflict_notice(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Notice display only.
if ( isset( $_GET['booking_conflict'] ) && '1' === $_GET['booking_conflict'] ) {
?>
*/
public static function get_booking_statuses(): array {
return array(
'pending' => __( 'Pending', 'wp-bnb' ),
'confirmed' => __( 'Confirmed', 'wp-bnb' ),
'checked_in' => __( 'Checked In', 'wp-bnb' ),
'checked_out' => __( 'Checked Out', 'wp-bnb' ),
'cancelled' => __( 'Cancelled', 'wp-bnb' ),
);
}
/**
* Get status color codes.
*
* @return array
*/
public static function get_status_colors(): array {
return array(
'pending' => '#dba617',
'confirmed' => '#00a32a',
'checked_in' => '#72aee6',
'checked_out' => '#646970',
'cancelled' => '#d63638',
);
}
/**
* Get valid status transitions.
*
* @return array>
*/
public static function get_status_transitions(): array {
return array(
'pending' => array( 'confirmed', 'cancelled' ),
'confirmed' => array( 'checked_in', 'cancelled' ),
'checked_in' => array( 'checked_out' ),
'checked_out' => array(),
'cancelled' => array( 'pending' ),
);
}
/**
* Check if a status transition is valid.
*
* @param string $from Current status.
* @param string $to New status.
* @return bool
*/
public static function can_transition_to( string $from, string $to ): bool {
if ( $from === $to ) {
return true;
}
$transitions = self::get_status_transitions();
return isset( $transitions[ $from ] ) && in_array( $to, $transitions[ $from ], true );
}
/**
* Generate a booking reference number.
*
* @return string
*/
public static function generate_reference(): string {
$year = gmdate( 'Y' );
$count = wp_count_posts( self::POST_TYPE );
$total = ( $count->publish ?? 0 ) + ( $count->draft ?? 0 ) + ( $count->pending ?? 0 ) + ( $count->trash ?? 0 ) + 1;
return sprintf( 'BNB-%s-%05d', $year, $total );
}
/**
* Calculate number of nights between two dates.
*
* @param string $check_in Check-in date (Y-m-d).
* @param string $check_out Check-out date (Y-m-d).
* @return int
*/
public static function calculate_nights( string $check_in, string $check_out ): int {
$start = new \DateTimeImmutable( $check_in );
$end = new \DateTimeImmutable( $check_out );
return max( 1, (int) $start->diff( $end )->days );
}
/**
* Check if dates conflict with existing bookings.
*
* @param int $room_id Room post ID.
* @param string $check_in Check-in date (Y-m-d).
* @param string $check_out Check-out date (Y-m-d).
* @param int|null $exclude_id Booking ID to exclude (for editing).
* @return bool
*/
public static function has_conflict( int $room_id, string $check_in, string $check_out, ?int $exclude_id = null ): bool {
$args = array(
'post_type' => self::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => 1,
'fields' => 'ids',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => self::META_PREFIX . 'room_id',
'value' => $room_id,
),
array(
'key' => self::META_PREFIX . 'status',
'value' => 'cancelled',
'compare' => '!=',
),
// Overlap detection: existing.check_in < new.check_out AND existing.check_out > new.check_in.
array(
'key' => self::META_PREFIX . 'check_in',
'value' => $check_out,
'compare' => '<',
'type' => 'DATE',
),
array(
'key' => self::META_PREFIX . 'check_out',
'value' => $check_in,
'compare' => '>',
'type' => 'DATE',
),
),
);
if ( $exclude_id ) {
$args['post__not_in'] = array( $exclude_id );
}
$conflicts = get_posts( $args );
return ! empty( $conflicts );
}
/**
* Get room for a booking.
*
* @param int $booking_id Booking post ID.
* @return \WP_Post|null
*/
public static function get_room( int $booking_id ): ?\WP_Post {
$room_id = get_post_meta( $booking_id, self::META_PREFIX . 'room_id', true );
if ( ! $room_id ) {
return null;
}
return get_post( $room_id );
}
/**
* Get building for a booking (through room).
*
* @param int $booking_id Booking post ID.
* @return \WP_Post|null
*/
public static function get_building( int $booking_id ): ?\WP_Post {
$room = self::get_room( $booking_id );
if ( ! $room ) {
return null;
}
return Room::get_building( $room->ID );
}
/**
* Get guest for a booking.
*
* Returns the linked Guest post if guest_id exists, or a stdClass object
* with guest data from booking meta for backward compatibility.
*
* @param int $booking_id Booking post ID.
* @return \WP_Post|\stdClass|null Guest post, virtual guest object, or null.
*/
public static function get_guest( int $booking_id ) {
$guest_id = get_post_meta( $booking_id, self::META_PREFIX . 'guest_id', true );
// If linked to Guest CPT, return the guest post.
if ( $guest_id ) {
$guest = get_post( $guest_id );
if ( $guest && Guest::POST_TYPE === $guest->post_type ) {
return $guest;
}
}
// Otherwise, create a virtual guest object from booking meta.
$guest_name = get_post_meta( $booking_id, self::META_PREFIX . 'guest_name', true );
$guest_email = get_post_meta( $booking_id, self::META_PREFIX . 'guest_email', true );
$guest_phone = get_post_meta( $booking_id, self::META_PREFIX . 'guest_phone', true );
if ( ! $guest_name && ! $guest_email ) {
return null;
}
// Return virtual guest object for backward compatibility.
$virtual_guest = new \stdClass();
$virtual_guest->ID = 0;
$virtual_guest->post_type = 'virtual_guest';
$virtual_guest->post_title = $guest_name ?: '';
$virtual_guest->name = $guest_name ?: '';
$virtual_guest->email = $guest_email ?: '';
$virtual_guest->phone = $guest_phone ?: '';
return $virtual_guest;
}
/**
* Get all bookings for a room.
*
* @param int $room_id Room post ID.
* @param array $args Additional query args.
* @return array<\WP_Post>
*/
public static function get_bookings_for_room( int $room_id, array $args = array() ): array {
$defaults = array(
'post_type' => self::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => self::META_PREFIX . 'room_id',
'value' => $room_id,
),
),
'orderby' => 'meta_value',
'meta_key' => self::META_PREFIX . 'check_in',
'order' => 'ASC',
);
return get_posts( array_merge( $defaults, $args ) );
}
/**
* Calculate total services cost for a booking.
*
* @param int $booking_id Booking post ID.
* @return float Total services cost.
*/
public static function calculate_booking_services_total( int $booking_id ): float {
$services = get_post_meta( $booking_id, self::SERVICES_META_KEY, true );
if ( ! is_array( $services ) || empty( $services ) ) {
return 0.0;
}
$total = 0.0;
foreach ( $services as $service ) {
if ( isset( $service['price'] ) ) {
$total += (float) $service['price'];
}
}
return $total;
}
/**
* Get selected services for a booking.
*
* @param int $booking_id Booking post ID.
* @return array Array of service data with names.
*/
public static function get_booking_services( int $booking_id ): array {
$services = get_post_meta( $booking_id, self::SERVICES_META_KEY, true );
if ( ! is_array( $services ) || empty( $services ) ) {
return array();
}
$result = array();
foreach ( $services as $service ) {
$service_post = get_post( $service['service_id'] ?? 0 );
if ( $service_post ) {
$result[] = array_merge(
$service,
array( 'name' => $service_post->post_title )
);
}
}
return $result;
}
/**
* Format price breakdown for display.
*
* @param array $breakdown Price breakdown array.
* @return string HTML output.
*/
private static function format_price_breakdown( array $breakdown ): string {
$output = '';
if ( isset( $breakdown['tier'] ) ) {
$output .= '- ' . esc_html__( 'Pricing Tier:', 'wp-bnb' ) . ' ';
$output .= esc_html( ucfirst( str_replace( '_', ' ', $breakdown['tier'] ) ) ) . '
';
}
if ( isset( $breakdown['nights'] ) && is_array( $breakdown['nights'] ) ) {
$output .= '- ' . esc_html__( 'Nights:', 'wp-bnb' ) . ' ' . count( $breakdown['nights'] ) . '
';
$output .= '- ' . esc_html__( 'Nightly Rate:', 'wp-bnb' ) . ' ';
$output .= esc_html( Calculator::formatPrice( (float) ( $breakdown['nightly_rate'] ?? 0 ) ) ) . '
';
} elseif ( isset( $breakdown['weeks'] ) ) {
$output .= '- ' . esc_html__( 'Weeks:', 'wp-bnb' ) . ' ' . esc_html( $breakdown['weeks'] ) . '
';
$output .= '- ' . esc_html__( 'Weekly Rate:', 'wp-bnb' ) . ' ';
$output .= esc_html( Calculator::formatPrice( (float) ( $breakdown['weekly_rate'] ?? 0 ) ) ) . '
';
} elseif ( isset( $breakdown['months'] ) ) {
$output .= '- ' . esc_html__( 'Months:', 'wp-bnb' ) . ' ' . esc_html( $breakdown['months'] ) . '
';
$output .= '- ' . esc_html__( 'Monthly Rate:', 'wp-bnb' ) . ' ';
$output .= esc_html( Calculator::formatPrice( (float) ( $breakdown['monthly_rate'] ?? 0 ) ) ) . '
';
}
if ( isset( $breakdown['total'] ) ) {
$output .= '- ' . esc_html__( 'Total:', 'wp-bnb' ) . ' ';
$output .= esc_html( Calculator::formatPrice( (float) $breakdown['total'] ) ) . '
';
}
$output .= '
';
return $output;
}
}