Add additional services system (v0.5.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m0s
All checks were successful
Create Release Package / build-release (push) Successful in 1m0s
- Service CPT with pricing types: Included, Per Booking, Per Night - ServiceCategory taxonomy with default categories - Booking-services integration with service selector - Real-time price calculation based on nights and quantity - Services total and grand total display in booking admin Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,13 @@ use Magdev\WpBnb\Pricing\Calculator;
|
||||
*/
|
||||
final class Booking {
|
||||
|
||||
/**
|
||||
* Services meta key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const SERVICES_META_KEY = '_bnb_booking_services';
|
||||
|
||||
/**
|
||||
* Post type slug.
|
||||
*
|
||||
@@ -125,6 +132,15 @@ final class Booking {
|
||||
'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' ),
|
||||
@@ -399,6 +415,111 @@ final class Booking {
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render services meta box.
|
||||
*
|
||||
* @param \WP_Post $post Current post object.
|
||||
* @return void
|
||||
*/
|
||||
public static function render_services_meta_box( \WP_Post $post ): void {
|
||||
$selected_services = get_post_meta( $post->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 ) ) {
|
||||
?>
|
||||
<p class="bnb-no-services-message">
|
||||
<?php esc_html_e( 'No services available.', 'wp-bnb' ); ?>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Service::POST_TYPE ) ); ?>">
|
||||
<?php esc_html_e( 'Add a service', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a lookup map for selected services.
|
||||
$selected_map = array();
|
||||
if ( is_array( $selected_services ) ) {
|
||||
foreach ( $selected_services as $service ) {
|
||||
if ( isset( $service['service_id'] ) ) {
|
||||
$selected_map[ $service['service_id'] ] = $service;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="bnb-services-selector" data-nights="<?php echo esc_attr( $nights ); ?>">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Select additional services for this booking.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
|
||||
<div class="bnb-services-list">
|
||||
<?php foreach ( $available_services as $service ) : ?>
|
||||
<?php
|
||||
$is_selected = isset( $selected_map[ $service['id'] ] );
|
||||
$quantity = $is_selected ? ( $selected_map[ $service['id'] ]['quantity'] ?? 1 ) : 1;
|
||||
$service_total = $is_selected
|
||||
? Service::calculate_service_price( $service['id'], $quantity, $nights )
|
||||
: 0;
|
||||
?>
|
||||
<div class="bnb-service-item <?php echo $is_selected ? 'selected' : ''; ?>"
|
||||
data-service-id="<?php echo esc_attr( $service['id'] ); ?>"
|
||||
data-price="<?php echo esc_attr( $service['price'] ); ?>"
|
||||
data-pricing-type="<?php echo esc_attr( $service['pricing_type'] ); ?>"
|
||||
data-max-quantity="<?php echo esc_attr( $service['max_quantity'] ); ?>">
|
||||
<label class="bnb-service-checkbox">
|
||||
<input type="checkbox" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][selected]"
|
||||
value="1" <?php checked( $is_selected ); ?>>
|
||||
<span class="bnb-service-name"><?php echo esc_html( $service['name'] ); ?></span>
|
||||
</label>
|
||||
<div class="bnb-service-details">
|
||||
<span class="bnb-service-price-label">
|
||||
<?php
|
||||
if ( 'included' === $service['pricing_type'] ) {
|
||||
echo '<span class="bnb-service-included-badge">' . esc_html__( 'Included', 'wp-bnb' ) . '</span>';
|
||||
} else {
|
||||
echo esc_html( $service['formatted_price'] );
|
||||
}
|
||||
?>
|
||||
</span>
|
||||
<?php if ( $service['max_quantity'] > 1 && 'included' !== $service['pricing_type'] ) : ?>
|
||||
<span class="bnb-service-quantity" <?php echo ! $is_selected ? 'style="display:none;"' : ''; ?>>
|
||||
<label>
|
||||
<?php esc_html_e( 'Qty:', 'wp-bnb' ); ?>
|
||||
<input type="number" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][quantity]"
|
||||
value="<?php echo esc_attr( $quantity ); ?>"
|
||||
min="1" max="<?php echo esc_attr( $service['max_quantity'] ); ?>"
|
||||
class="small-text bnb-service-qty-input">
|
||||
</label>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<input type="hidden" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][quantity]" value="1">
|
||||
<?php endif; ?>
|
||||
<span class="bnb-service-line-total" <?php echo ( ! $is_selected || $service_total <= 0 ) ? 'style="display:none;"' : ''; ?>>
|
||||
= <strong class="bnb-service-total-value"><?php echo esc_html( Calculator::formatPrice( $service_total ) ); ?></strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="bnb-services-total">
|
||||
<strong><?php esc_html_e( 'Services Total:', 'wp-bnb' ); ?></strong>
|
||||
<span id="bnb-services-total-amount">
|
||||
<?php
|
||||
$services_total = self::calculate_booking_services_total( $post->ID );
|
||||
echo esc_html( Calculator::formatPrice( $services_total ) );
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render pricing meta box.
|
||||
*
|
||||
@@ -447,6 +568,36 @@ final class Booking {
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$services_total = self::calculate_booking_services_total( $post->ID );
|
||||
if ( $services_total > 0 ) :
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php esc_html_e( 'Services', 'wp-bnb' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<div class="bnb-booking-services-summary">
|
||||
<?php echo esc_html( Calculator::formatPrice( $services_total ) ); ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php esc_html_e( 'Grand Total', 'wp-bnb' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<div class="bnb-booking-grand-total">
|
||||
<strong>
|
||||
<?php
|
||||
$room_price = (float) $calculated_price;
|
||||
echo esc_html( Calculator::formatPrice( $room_price + $services_total ) );
|
||||
?>
|
||||
</strong>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="bnb_booking_override_price"><?php esc_html_e( 'Override Price', 'wp-bnb' ); ?></label>
|
||||
@@ -678,6 +829,43 @@ final class Booking {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ) {
|
||||
/**
|
||||
@@ -794,13 +982,21 @@ final class Booking {
|
||||
break;
|
||||
|
||||
case 'price':
|
||||
$price = get_post_meta( $post_id, self::META_PREFIX . 'calculated_price', true );
|
||||
if ( $price ) {
|
||||
echo esc_html( Calculator::formatPrice( (float) $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 ' <span class="bnb-price-override" title="' . esc_attr__( 'Price manually overridden', 'wp-bnb' ) . '">*</span>';
|
||||
}
|
||||
if ( $services_total > 0 ) {
|
||||
echo '<br><small style="color: #646970;">' . esc_html__( 'incl. services', 'wp-bnb' ) . '</small>';
|
||||
}
|
||||
} elseif ( $room_price > 0 ) {
|
||||
echo esc_html( Calculator::formatPrice( $room_price ) );
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
@@ -1232,6 +1428,56 @@ final class Booking {
|
||||
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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user