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,10 +18,12 @@ use Magdev\WpBnb\PostTypes\Booking;
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Guest;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\PostTypes\Service;
|
||||
use Magdev\WpBnb\Privacy\Manager as PrivacyManager;
|
||||
use Magdev\WpBnb\Pricing\Season;
|
||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||
use Magdev\WpBnb\Taxonomies\ServiceCategory;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
@@ -95,6 +97,7 @@ final class Plugin {
|
||||
Room::init();
|
||||
Booking::init();
|
||||
Guest::init();
|
||||
Service::init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,6 +109,7 @@ final class Plugin {
|
||||
// Taxonomies must be registered before post types that use them.
|
||||
Amenity::init();
|
||||
RoomType::init();
|
||||
ServiceCategory::init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +192,7 @@ final class Plugin {
|
||||
|
||||
// Check if we're on plugin pages or editing our custom post types.
|
||||
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
||||
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE ), true );
|
||||
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE, Service::POST_TYPE ), true );
|
||||
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
||||
|
||||
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
||||
@@ -226,26 +230,28 @@ final class Plugin {
|
||||
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
||||
'postType' => $post_type,
|
||||
'i18n' => array(
|
||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
||||
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
||||
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
||||
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
||||
'increase' => __( 'increase', 'wp-bnb' ),
|
||||
'discount' => __( 'discount', 'wp-bnb' ),
|
||||
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
||||
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
||||
'available' => __( 'Available', 'wp-bnb' ),
|
||||
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
||||
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
||||
'nights' => __( 'nights', 'wp-bnb' ),
|
||||
'night' => __( 'night', 'wp-bnb' ),
|
||||
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
||||
'searchingGuests' => __( 'Searching...', 'wp-bnb' ),
|
||||
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
|
||||
'selectGuest' => __( 'Select', 'wp-bnb' ),
|
||||
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
||||
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
||||
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
||||
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
||||
'increase' => __( 'increase', 'wp-bnb' ),
|
||||
'discount' => __( 'discount', 'wp-bnb' ),
|
||||
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
||||
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
||||
'available' => __( 'Available', 'wp-bnb' ),
|
||||
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
||||
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
||||
'nights' => __( 'nights', 'wp-bnb' ),
|
||||
'night' => __( 'night', 'wp-bnb' ),
|
||||
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
||||
'searchingGuests' => __( 'Searching...', 'wp-bnb' ),
|
||||
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
|
||||
'selectGuest' => __( 'Select', 'wp-bnb' ),
|
||||
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
||||
'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ),
|
||||
'perBookingDescription' => __( 'This price will be charged once for the booking.', 'wp-bnb' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
623
src/PostTypes/Service.php
Normal file
623
src/PostTypes/Service.php
Normal file
@@ -0,0 +1,623 @@
|
||||
<?php
|
||||
/**
|
||||
* Service post type.
|
||||
*
|
||||
* Custom post type for BnB additional services.
|
||||
*
|
||||
* @package Magdev\WpBnb\PostTypes
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\PostTypes;
|
||||
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
|
||||
/**
|
||||
* Service post type class.
|
||||
*/
|
||||
final class Service {
|
||||
|
||||
/**
|
||||
* Post type slug.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POST_TYPE = 'bnb_service';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_bnb_service_';
|
||||
|
||||
/**
|
||||
* Initialize the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action( 'init', array( self::class, 'register' ) );
|
||||
add_action( 'add_meta_boxes', array( self::class, 'add_meta_boxes' ) );
|
||||
add_action( 'save_post_' . self::POST_TYPE, array( self::class, 'save_meta' ), 10, 2 );
|
||||
add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( self::class, 'add_columns' ) );
|
||||
add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( self::class, 'render_column' ), 10, 2 );
|
||||
add_filter( 'manage_edit-' . self::POST_TYPE . '_sortable_columns', array( self::class, 'sortable_columns' ) );
|
||||
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Services', 'post type general name', 'wp-bnb' ),
|
||||
'singular_name' => _x( 'Service', 'post type singular name', 'wp-bnb' ),
|
||||
'menu_name' => _x( 'Services', 'admin menu', 'wp-bnb' ),
|
||||
'name_admin_bar' => _x( 'Service', 'add new on admin bar', 'wp-bnb' ),
|
||||
'add_new' => _x( 'Add New', 'service', 'wp-bnb' ),
|
||||
'add_new_item' => __( 'Add New Service', 'wp-bnb' ),
|
||||
'new_item' => __( 'New Service', 'wp-bnb' ),
|
||||
'edit_item' => __( 'Edit Service', 'wp-bnb' ),
|
||||
'view_item' => __( 'View Service', 'wp-bnb' ),
|
||||
'all_items' => __( 'Services', 'wp-bnb' ),
|
||||
'search_items' => __( 'Search Services', 'wp-bnb' ),
|
||||
'parent_item_colon' => __( 'Parent Services:', 'wp-bnb' ),
|
||||
'not_found' => __( 'No services found.', 'wp-bnb' ),
|
||||
'not_found_in_trash' => __( 'No services found in Trash.', 'wp-bnb' ),
|
||||
'archives' => __( 'Service archives', 'wp-bnb' ),
|
||||
'insert_into_item' => __( 'Insert into service', 'wp-bnb' ),
|
||||
'uploaded_to_this_item' => __( 'Uploaded to this service', 'wp-bnb' ),
|
||||
'filter_items_list' => __( 'Filter services list', 'wp-bnb' ),
|
||||
'items_list_navigation' => __( 'Services list navigation', 'wp-bnb' ),
|
||||
'items_list' => __( 'Services 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-plus-alt',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'services',
|
||||
'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_service_pricing',
|
||||
__( 'Pricing', 'wp-bnb' ),
|
||||
array( self::class, 'render_pricing_meta_box' ),
|
||||
self::POST_TYPE,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'bnb_service_settings',
|
||||
__( 'Service Settings', 'wp-bnb' ),
|
||||
array( self::class, 'render_settings_meta_box' ),
|
||||
self::POST_TYPE,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render pricing meta box.
|
||||
*
|
||||
* @param \WP_Post $post Current post object.
|
||||
* @return void
|
||||
*/
|
||||
public static function render_pricing_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'bnb_service_meta', 'bnb_service_meta_nonce' );
|
||||
|
||||
$pricing_type = get_post_meta( $post->ID, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||
$price = get_post_meta( $post->ID, self::META_PREFIX . 'price', true );
|
||||
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="bnb_service_pricing_type"><?php esc_html_e( 'Pricing Type', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="radio" name="bnb_service_pricing_type" value="included"
|
||||
<?php checked( $pricing_type, 'included' ); ?>>
|
||||
<?php esc_html_e( 'Included (Free)', 'wp-bnb' ); ?>
|
||||
<p class="description"><?php esc_html_e( 'Service is included at no extra cost.', 'wp-bnb' ); ?></p>
|
||||
</label>
|
||||
<br><br>
|
||||
<label>
|
||||
<input type="radio" name="bnb_service_pricing_type" value="per_booking"
|
||||
<?php checked( $pricing_type, 'per_booking' ); ?>>
|
||||
<?php esc_html_e( 'Per Booking (One-time)', 'wp-bnb' ); ?>
|
||||
<p class="description"><?php esc_html_e( 'Fixed price charged once per booking.', 'wp-bnb' ); ?></p>
|
||||
</label>
|
||||
<br><br>
|
||||
<label>
|
||||
<input type="radio" name="bnb_service_pricing_type" value="per_night"
|
||||
<?php checked( $pricing_type, 'per_night' ); ?>>
|
||||
<?php esc_html_e( 'Per Night', 'wp-bnb' ); ?>
|
||||
<p class="description"><?php esc_html_e( 'Price multiplied by the number of nights.', 'wp-bnb' ); ?></p>
|
||||
</label>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="bnb-service-price-row" <?php echo 'included' === $pricing_type ? 'style="display:none;"' : ''; ?>>
|
||||
<th scope="row">
|
||||
<label for="bnb_service_price"><?php esc_html_e( 'Price', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<div class="bnb-price-input-wrapper">
|
||||
<input type="number" id="bnb_service_price" name="bnb_service_price"
|
||||
value="<?php echo esc_attr( $price ); ?>" class="small-text"
|
||||
min="0" step="0.01">
|
||||
<span class="bnb-price-unit"><?php echo esc_html( $currency ); ?></span>
|
||||
<span id="bnb-service-price-suffix"></span>
|
||||
</div>
|
||||
<p class="description" id="bnb-service-price-description">
|
||||
<?php
|
||||
if ( 'per_night' === $pricing_type ) {
|
||||
esc_html_e( 'This price will be charged per night of the stay.', 'wp-bnb' );
|
||||
} else {
|
||||
esc_html_e( 'This price will be charged once for the booking.', 'wp-bnb' );
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render settings meta box.
|
||||
*
|
||||
* @param \WP_Post $post Current post object.
|
||||
* @return void
|
||||
*/
|
||||
public static function render_settings_meta_box( \WP_Post $post ): void {
|
||||
$status = get_post_meta( $post->ID, self::META_PREFIX . 'status', true ) ?: 'active';
|
||||
$sort_order = get_post_meta( $post->ID, self::META_PREFIX . 'sort_order', true ) ?: 0;
|
||||
$max_qty = get_post_meta( $post->ID, self::META_PREFIX . 'max_quantity', true ) ?: 1;
|
||||
?>
|
||||
<p>
|
||||
<label for="bnb_service_status"><strong><?php esc_html_e( 'Status', 'wp-bnb' ); ?></strong></label>
|
||||
</p>
|
||||
<select id="bnb_service_status" name="bnb_service_status" class="widefat">
|
||||
<option value="active" <?php selected( $status, 'active' ); ?>><?php esc_html_e( 'Active', 'wp-bnb' ); ?></option>
|
||||
<option value="inactive" <?php selected( $status, 'inactive' ); ?>><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></option>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'Inactive services cannot be added to bookings.', 'wp-bnb' ); ?></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
<label for="bnb_service_sort_order"><strong><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></strong></label>
|
||||
</p>
|
||||
<input type="number" id="bnb_service_sort_order" name="bnb_service_sort_order"
|
||||
value="<?php echo esc_attr( $sort_order ); ?>" class="small-text" min="0">
|
||||
<p class="description"><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
<label for="bnb_service_max_quantity"><strong><?php esc_html_e( 'Maximum Quantity', 'wp-bnb' ); ?></strong></label>
|
||||
</p>
|
||||
<input type="number" id="bnb_service_max_quantity" name="bnb_service_max_quantity"
|
||||
value="<?php echo esc_attr( $max_qty ); ?>" class="small-text" min="1" max="99">
|
||||
<p class="description"><?php esc_html_e( 'Maximum times this service can be added to a booking.', 'wp-bnb' ); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public static function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
// Verify nonce.
|
||||
if ( ! isset( $_POST['bnb_service_meta_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_key( $_POST['bnb_service_meta_nonce'] ), 'bnb_service_meta' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check autosave.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permissions.
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pricing type.
|
||||
$valid_pricing_types = array( 'included', 'per_booking', 'per_night' );
|
||||
$pricing_type = isset( $_POST['bnb_service_pricing_type'] )
|
||||
? sanitize_text_field( wp_unslash( $_POST['bnb_service_pricing_type'] ) )
|
||||
: 'per_booking';
|
||||
if ( in_array( $pricing_type, $valid_pricing_types, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'pricing_type', $pricing_type );
|
||||
}
|
||||
|
||||
// Price (not required for 'included').
|
||||
if ( 'included' === $pricing_type ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'price', 0 );
|
||||
} elseif ( isset( $_POST['bnb_service_price'] ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'price', floatval( $_POST['bnb_service_price'] ) );
|
||||
}
|
||||
|
||||
// Status.
|
||||
$valid_statuses = array( 'active', 'inactive' );
|
||||
$status = isset( $_POST['bnb_service_status'] )
|
||||
? sanitize_text_field( wp_unslash( $_POST['bnb_service_status'] ) )
|
||||
: 'active';
|
||||
if ( in_array( $status, $valid_statuses, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'status', $status );
|
||||
}
|
||||
|
||||
// Sort order.
|
||||
if ( isset( $_POST['bnb_service_sort_order'] ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'sort_order', absint( $_POST['bnb_service_sort_order'] ) );
|
||||
}
|
||||
|
||||
// Max quantity.
|
||||
if ( isset( $_POST['bnb_service_max_quantity'] ) ) {
|
||||
$max_qty = absint( $_POST['bnb_service_max_quantity'] );
|
||||
$max_qty = max( 1, min( 99, $max_qty ) );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'max_quantity', $max_qty );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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['pricing_type'] = __( 'Pricing Type', 'wp-bnb' );
|
||||
$new_columns['price'] = __( 'Price', 'wp-bnb' );
|
||||
$new_columns['service_status'] = __( 'Status', 'wp-bnb' );
|
||||
}
|
||||
}
|
||||
// Remove 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 'pricing_type':
|
||||
$pricing_type = get_post_meta( $post_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||
$labels = self::get_pricing_type_labels();
|
||||
$icons = array(
|
||||
'included' => 'yes-alt',
|
||||
'per_booking' => 'tag',
|
||||
'per_night' => 'calendar-alt',
|
||||
);
|
||||
$colors = array(
|
||||
'included' => '#00a32a',
|
||||
'per_booking' => '#135e96',
|
||||
'per_night' => '#dba617',
|
||||
);
|
||||
echo '<span class="dashicons dashicons-' . esc_attr( $icons[ $pricing_type ] ?? 'admin-generic' ) . '" style="color: ' . esc_attr( $colors[ $pricing_type ] ?? '#646970' ) . '; vertical-align: middle; margin-right: 3px;"></span>';
|
||||
echo esc_html( $labels[ $pricing_type ] ?? $pricing_type );
|
||||
break;
|
||||
|
||||
case 'price':
|
||||
$pricing_type = get_post_meta( $post_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||
if ( 'included' === $pricing_type ) {
|
||||
echo '<span class="bnb-service-included">' . esc_html__( 'Included', 'wp-bnb' ) . '</span>';
|
||||
} else {
|
||||
$price = get_post_meta( $post_id, self::META_PREFIX . 'price', true );
|
||||
if ( $price ) {
|
||||
echo esc_html( Calculator::formatPrice( (float) $price ) );
|
||||
if ( 'per_night' === $pricing_type ) {
|
||||
echo ' <small style="color: #646970;">' . esc_html__( '/ night', 'wp-bnb' ) . '</small>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="bnb-no-price">' . esc_html__( 'Not set', 'wp-bnb' ) . '</span>';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'service_status':
|
||||
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'active';
|
||||
$classes = array(
|
||||
'active' => 'bnb-service-status-active',
|
||||
'inactive' => 'bnb-service-status-inactive',
|
||||
);
|
||||
$labels = array(
|
||||
'active' => __( 'Active', 'wp-bnb' ),
|
||||
'inactive' => __( 'Inactive', 'wp-bnb' ),
|
||||
);
|
||||
echo '<span class="bnb-service-status ' . esc_attr( $classes[ $status ] ?? '' ) . '">';
|
||||
echo esc_html( $labels[ $status ] ?? $status );
|
||||
echo '</span>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sortable columns.
|
||||
*
|
||||
* @param array $columns Existing sortable columns.
|
||||
* @return array
|
||||
*/
|
||||
public static function sortable_columns( array $columns ): array {
|
||||
$columns['price'] = 'price';
|
||||
$columns['service_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['service_status'] ) ? sanitize_text_field( wp_unslash( $_GET['service_status'] ) ) : '';
|
||||
?>
|
||||
<select name="service_status">
|
||||
<option value=""><?php esc_html_e( 'All Statuses', 'wp-bnb' ); ?></option>
|
||||
<option value="active" <?php selected( $selected_status, 'active' ); ?>><?php esc_html_e( 'Active', 'wp-bnb' ); ?></option>
|
||||
<option value="inactive" <?php selected( $selected_status, 'inactive' ); ?>><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></option>
|
||||
</select>
|
||||
<?php
|
||||
|
||||
// Pricing type filter.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
|
||||
$selected_pricing = isset( $_GET['pricing_type'] ) ? sanitize_text_field( wp_unslash( $_GET['pricing_type'] ) ) : '';
|
||||
$labels = self::get_pricing_type_labels();
|
||||
?>
|
||||
<select name="pricing_type">
|
||||
<option value=""><?php esc_html_e( 'All Pricing Types', 'wp-bnb' ); ?></option>
|
||||
<?php foreach ( $labels as $value => $label ) : ?>
|
||||
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_pricing, $value ); ?>>
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter services by status and pricing type in admin list.
|
||||
*
|
||||
* @param \WP_Query $query Current query.
|
||||
* @return void
|
||||
*/
|
||||
public static function filter_query( \WP_Query $query ): void {
|
||||
if ( ! is_admin() || ! $query->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['service_status'] ) ) {
|
||||
$meta_query[] = array(
|
||||
'key' => self::META_PREFIX . 'status',
|
||||
'value' => sanitize_text_field( wp_unslash( $_GET['service_status'] ) ),
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||
if ( ! empty( $_GET['pricing_type'] ) ) {
|
||||
$meta_query[] = array(
|
||||
'key' => self::META_PREFIX . 'pricing_type',
|
||||
'value' => sanitize_text_field( wp_unslash( $_GET['pricing_type'] ) ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $meta_query ) ) {
|
||||
$meta_query['relation'] = 'AND';
|
||||
$query->set( 'meta_query', $meta_query );
|
||||
}
|
||||
|
||||
// Handle sorting.
|
||||
$orderby = $query->get( 'orderby' );
|
||||
if ( 'price' === $orderby ) {
|
||||
$query->set( 'meta_key', self::META_PREFIX . 'price' );
|
||||
$query->set( 'orderby', 'meta_value_num' );
|
||||
} 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 __( 'Service name', 'wp-bnb' );
|
||||
}
|
||||
return $placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pricing type labels.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function get_pricing_type_labels(): array {
|
||||
return array(
|
||||
'included' => __( 'Included (Free)', 'wp-bnb' ),
|
||||
'per_booking' => __( 'Per Booking', 'wp-bnb' ),
|
||||
'per_night' => __( 'Per Night', 'wp-bnb' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active services.
|
||||
*
|
||||
* @param array $args Additional query args.
|
||||
* @return array<\WP_Post>
|
||||
*/
|
||||
public static function get_active_services( 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 . 'status',
|
||||
'value' => 'active',
|
||||
),
|
||||
),
|
||||
'meta_key' => self::META_PREFIX . 'sort_order',
|
||||
'orderby' => 'meta_value_num',
|
||||
'order' => 'ASC',
|
||||
);
|
||||
|
||||
return get_posts( array_merge( $defaults, $args ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service data for a service.
|
||||
*
|
||||
* @param int $service_id Service post ID.
|
||||
* @return array|null Service data or null if not found.
|
||||
*/
|
||||
public static function get_service_data( int $service_id ): ?array {
|
||||
$service = get_post( $service_id );
|
||||
if ( ! $service || self::POST_TYPE !== $service->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $service_id,
|
||||
'name' => $service->post_title,
|
||||
'description' => $service->post_content,
|
||||
'pricing_type' => get_post_meta( $service_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking',
|
||||
'price' => (float) get_post_meta( $service_id, self::META_PREFIX . 'price', true ),
|
||||
'status' => get_post_meta( $service_id, self::META_PREFIX . 'status', true ) ?: 'active',
|
||||
'sort_order' => (int) get_post_meta( $service_id, self::META_PREFIX . 'sort_order', true ),
|
||||
'max_quantity' => (int) get_post_meta( $service_id, self::META_PREFIX . 'max_quantity', true ) ?: 1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate service price for a booking.
|
||||
*
|
||||
* @param int $service_id Service post ID.
|
||||
* @param int $quantity Quantity of the service.
|
||||
* @param int $nights Number of nights (for per-night pricing).
|
||||
* @return float Calculated price.
|
||||
*/
|
||||
public static function calculate_service_price( int $service_id, int $quantity = 1, int $nights = 1 ): float {
|
||||
$data = self::get_service_data( $service_id );
|
||||
if ( ! $data ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if ( 'included' === $data['pricing_type'] ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$base_price = $data['price'];
|
||||
|
||||
if ( 'per_night' === $data['pricing_type'] ) {
|
||||
return $base_price * $quantity * max( 1, $nights );
|
||||
}
|
||||
|
||||
// per_booking.
|
||||
return $base_price * $quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get services for booking display/selection.
|
||||
*
|
||||
* @return array Array of services with their data.
|
||||
*/
|
||||
public static function get_services_for_booking(): array {
|
||||
$services = self::get_active_services();
|
||||
$result = array();
|
||||
|
||||
foreach ( $services as $service ) {
|
||||
$data = self::get_service_data( $service->ID );
|
||||
if ( $data ) {
|
||||
$data['formatted_price'] = self::format_service_price( $data );
|
||||
$result[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format service price for display.
|
||||
*
|
||||
* @param array $service_data Service data array.
|
||||
* @return string Formatted price string.
|
||||
*/
|
||||
public static function format_service_price( array $service_data ): string {
|
||||
if ( 'included' === $service_data['pricing_type'] ) {
|
||||
return __( 'Included', 'wp-bnb' );
|
||||
}
|
||||
|
||||
$formatted = Calculator::formatPrice( $service_data['price'] );
|
||||
|
||||
if ( 'per_night' === $service_data['pricing_type'] ) {
|
||||
/* translators: %s: Formatted price */
|
||||
return sprintf( __( '%s / night', 'wp-bnb' ), $formatted );
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
}
|
||||
276
src/Taxonomies/ServiceCategory.php
Normal file
276
src/Taxonomies/ServiceCategory.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/**
|
||||
* Service Category taxonomy.
|
||||
*
|
||||
* Non-hierarchical taxonomy for categorizing additional services.
|
||||
*
|
||||
* @package Magdev\WpBnb\Taxonomies
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Taxonomies;
|
||||
|
||||
/**
|
||||
* Service Category taxonomy class.
|
||||
*/
|
||||
final class ServiceCategory {
|
||||
|
||||
/**
|
||||
* Taxonomy slug.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TAXONOMY = 'bnb_service_category';
|
||||
|
||||
/**
|
||||
* Initialize the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action( 'init', array( self::class, 'register' ) );
|
||||
add_action( 'bnb_service_category_add_form_fields', array( self::class, 'add_form_fields' ) );
|
||||
add_action( 'bnb_service_category_edit_form_fields', array( self::class, 'edit_form_fields' ), 10, 2 );
|
||||
add_action( 'created_bnb_service_category', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||
add_action( 'edited_bnb_service_category', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||
add_filter( 'manage_edit-bnb_service_category_columns', array( self::class, 'add_columns' ) );
|
||||
add_filter( 'manage_bnb_service_category_custom_column', array( self::class, 'render_column' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Service Categories', 'taxonomy general name', 'wp-bnb' ),
|
||||
'singular_name' => _x( 'Service Category', 'taxonomy singular name', 'wp-bnb' ),
|
||||
'search_items' => __( 'Search Service Categories', 'wp-bnb' ),
|
||||
'popular_items' => __( 'Popular Service Categories', 'wp-bnb' ),
|
||||
'all_items' => __( 'All Service Categories', 'wp-bnb' ),
|
||||
'parent_item' => null,
|
||||
'parent_item_colon' => null,
|
||||
'edit_item' => __( 'Edit Service Category', 'wp-bnb' ),
|
||||
'update_item' => __( 'Update Service Category', 'wp-bnb' ),
|
||||
'add_new_item' => __( 'Add New Service Category', 'wp-bnb' ),
|
||||
'new_item_name' => __( 'New Service Category Name', 'wp-bnb' ),
|
||||
'separate_items_with_commas' => __( 'Separate categories with commas', 'wp-bnb' ),
|
||||
'add_or_remove_items' => __( 'Add or remove categories', 'wp-bnb' ),
|
||||
'choose_from_most_used' => __( 'Choose from the most used categories', 'wp-bnb' ),
|
||||
'not_found' => __( 'No service categories found.', 'wp-bnb' ),
|
||||
'menu_name' => __( 'Categories', 'wp-bnb' ),
|
||||
'back_to_items' => __( '← Back to Categories', 'wp-bnb' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'hierarchical' => false, // Non-hierarchical (like tags).
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => true,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_in_rest' => true,
|
||||
'show_tagcloud' => false,
|
||||
'show_in_quick_edit' => true,
|
||||
'show_admin_column' => true,
|
||||
'rewrite' => array(
|
||||
'slug' => 'service-category',
|
||||
'with_front' => false,
|
||||
),
|
||||
'query_var' => true,
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_options',
|
||||
'edit_terms' => 'manage_options',
|
||||
'delete_terms' => 'manage_options',
|
||||
'assign_terms' => 'edit_posts',
|
||||
),
|
||||
);
|
||||
|
||||
register_taxonomy( self::TAXONOMY, array( 'bnb_service' ), $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom fields to the add term form.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_form_fields(): void {
|
||||
?>
|
||||
<div class="form-field term-icon-wrap">
|
||||
<label for="service-category-icon"><?php esc_html_e( 'Icon', 'wp-bnb' ); ?></label>
|
||||
<select name="service_category_icon" id="service-category-icon">
|
||||
<?php foreach ( self::get_icon_options() as $value => $label ) : ?>
|
||||
<option value="<?php echo esc_attr( $value ); ?>"><?php echo esc_html( $label ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p><?php esc_html_e( 'Select an icon to represent this category.', 'wp-bnb' ); ?></p>
|
||||
</div>
|
||||
<div class="form-field term-sort-order-wrap">
|
||||
<label for="service-category-sort-order"><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></label>
|
||||
<input type="number" name="service_category_sort_order" id="service-category-sort-order" value="0" min="0">
|
||||
<p><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom fields to the edit term form.
|
||||
*
|
||||
* @param \WP_Term $term Current term object.
|
||||
* @param string $taxonomy Current taxonomy slug.
|
||||
* @return void
|
||||
*/
|
||||
public static function edit_form_fields( \WP_Term $term, string $taxonomy ): void {
|
||||
$icon = get_term_meta( $term->term_id, 'service_category_icon', true );
|
||||
$sort_order = get_term_meta( $term->term_id, 'service_category_sort_order', true );
|
||||
?>
|
||||
<tr class="form-field term-icon-wrap">
|
||||
<th scope="row">
|
||||
<label for="service-category-icon"><?php esc_html_e( 'Icon', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="service_category_icon" id="service-category-icon">
|
||||
<?php foreach ( self::get_icon_options() as $value => $label ) : ?>
|
||||
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $icon, $value ); ?>>
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'Select an icon to represent this category.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="form-field term-sort-order-wrap">
|
||||
<th scope="row">
|
||||
<label for="service-category-sort-order"><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="service_category_sort_order" id="service-category-sort-order"
|
||||
value="<?php echo esc_attr( $sort_order ?: '0' ); ?>" min="0">
|
||||
<p class="description"><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save term meta data.
|
||||
*
|
||||
* @param int $term_id Term ID.
|
||||
* @param int $tt_id Term taxonomy ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function save_term_meta( int $term_id, int $tt_id ): void {
|
||||
if ( isset( $_POST['service_category_icon'] ) ) {
|
||||
update_term_meta(
|
||||
$term_id,
|
||||
'service_category_icon',
|
||||
sanitize_text_field( wp_unslash( $_POST['service_category_icon'] ) )
|
||||
);
|
||||
}
|
||||
if ( isset( $_POST['service_category_sort_order'] ) ) {
|
||||
update_term_meta(
|
||||
$term_id,
|
||||
'service_category_sort_order',
|
||||
absint( $_POST['service_category_sort_order'] )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom columns to the taxonomy 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 ( 'name' === $key ) {
|
||||
$new_columns['icon'] = __( 'Icon', 'wp-bnb' );
|
||||
$new_columns['sort_order'] = __( 'Sort Order', 'wp-bnb' );
|
||||
}
|
||||
}
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom column content.
|
||||
*
|
||||
* @param string $content Column content.
|
||||
* @param string $column_name Column name.
|
||||
* @param int $term_id Term ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function render_column( string $content, string $column_name, int $term_id ): string {
|
||||
if ( 'icon' === $column_name ) {
|
||||
$icon = get_term_meta( $term_id, 'service_category_icon', true );
|
||||
if ( $icon ) {
|
||||
return '<span class="dashicons dashicons-' . esc_attr( $icon ) . '"></span>';
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
if ( 'sort_order' === $column_name ) {
|
||||
$sort_order = get_term_meta( $term_id, 'service_category_sort_order', true );
|
||||
return esc_html( $sort_order ?: '0' );
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available icon options.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function get_icon_options(): array {
|
||||
return array(
|
||||
'' => __( '— Select Icon —', 'wp-bnb' ),
|
||||
'food' => __( 'Food & Dining', 'wp-bnb' ),
|
||||
'car' => __( 'Transportation', 'wp-bnb' ),
|
||||
'heart' => __( 'Wellness & Spa', 'wp-bnb' ),
|
||||
'tickets-alt' => __( 'Activities', 'wp-bnb' ),
|
||||
'admin-home' => __( 'Housekeeping', 'wp-bnb' ),
|
||||
'admin-appearance' => __( 'Room Service', 'wp-bnb' ),
|
||||
'store' => __( 'Shopping', 'wp-bnb' ),
|
||||
'groups' => __( 'Childcare', 'wp-bnb' ),
|
||||
'pets' => __( 'Pet Services', 'wp-bnb' ),
|
||||
'businessman' => __( 'Business', 'wp-bnb' ),
|
||||
'calendar' => __( 'Events', 'wp-bnb' ),
|
||||
'camera' => __( 'Photography', 'wp-bnb' ),
|
||||
'admin-generic' => __( 'Other', 'wp-bnb' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default service categories to seed on activation.
|
||||
*
|
||||
* @return array<string, array{icon: string, sort_order: int}>
|
||||
*/
|
||||
public static function get_default_terms(): array {
|
||||
return array(
|
||||
__( 'Food & Dining', 'wp-bnb' ) => array(
|
||||
'icon' => 'food',
|
||||
'sort_order' => 10,
|
||||
),
|
||||
__( 'Transportation', 'wp-bnb' ) => array(
|
||||
'icon' => 'car',
|
||||
'sort_order' => 20,
|
||||
),
|
||||
__( 'Wellness & Spa', 'wp-bnb' ) => array(
|
||||
'icon' => 'heart',
|
||||
'sort_order' => 30,
|
||||
),
|
||||
__( 'Activities', 'wp-bnb' ) => array(
|
||||
'icon' => 'tickets-alt',
|
||||
'sort_order' => 40,
|
||||
),
|
||||
__( 'Housekeeping', 'wp-bnb' ) => array(
|
||||
'icon' => 'admin-home',
|
||||
'sort_order' => 50,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user