diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e8d73..439e081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.0] - 2026-01-31 + +### Added + +- Custom Post Type: Buildings (`bnb_building`) + - Address fields (street, city, state, ZIP, country) + - Contact information (phone, email, website) + - Building details (total rooms, floors, year built) + - Check-in/check-out time configuration + - Featured image support + - Custom admin columns (city, country, room count) + - Sortable columns +- Custom Post Type: Rooms (`bnb_room`) + - Building relationship (parent building selection) + - Room details (number, floor, size, capacity) + - Guest capacity (total, max adults, max children) + - Beds description and bathroom count + - Room status (available, occupied, maintenance, blocked) + - Image gallery with drag-and-drop sorting + - Featured image support + - Custom admin columns (building, room number, type, capacity, status) + - Building filter dropdown in admin list +- Custom Taxonomy: Room Types (`bnb_room_type`) + - Hierarchical (category-like) structure + - Default types: Standard, Superior, Suite, Family, Accessible, Apartment + - Subtypes: Single, Double, Twin, Junior Suite, Executive Suite + - Base capacity meta field + - Sort order meta field +- Custom Taxonomy: Amenities (`bnb_amenity`) + - Non-hierarchical (tag-like) structure + - Default amenities: WiFi, Parking, Breakfast, TV, A/C, Pet Friendly, etc. + - Dashicon selection for visual display + - Custom column showing icon +- Admin enhancements + - Gallery meta box with media library integration + - Status badges with color coding + - Custom title placeholders for each post type + - Post type edit screens with proper asset loading + +### Changed + +- Updated admin assets to handle post type edit screens +- Enhanced asset enqueuing to include jQuery UI Sortable for galleries +- Improved localization with additional i18n strings + ## [0.0.1] - 2026-01-31 ### Added @@ -35,4 +80,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Input sanitization and output escaping - Server secret masking in license settings +[0.1.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.1.0 [0.0.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.0.1 diff --git a/CLAUDE.md b/CLAUDE.md index d10b06d..352bb0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -215,8 +215,14 @@ wp-bnb/ │ └── release.yml # CI/CD release pipeline ├── src/ # PHP source code (PSR-4: Magdev\WpBnb) │ ├── Plugin.php # Main plugin singleton -│ └── License/ -│ └── Manager.php # License management +│ ├── License/ +│ │ └── Manager.php # License management +│ ├── PostTypes/ # Custom post types +│ │ ├── Building.php # Building post type +│ │ └── Room.php # Room post type +│ └── Taxonomies/ # Custom taxonomies +│ ├── Amenity.php # Amenity taxonomy (tags) +│ └── RoomType.php # Room type taxonomy (categories) ├── lib/ # Git submodules │ └── wc-licensed-product-client/ # License client library ├── vendor/ # Composer dependencies (auto-generated) @@ -303,3 +309,44 @@ Admin features always work; frontend requires valid license. - Settings page uses tabs with nonce-protected form submission - AJAX handlers require `check_ajax_referer()` and `current_user_can()` checks - CI/CD workflow excludes `lib/` directory but includes `vendor/` in releases + +### 2026-01-31 - Version 0.1.0 (Core Data Structures) + +**Completed:** + +- Created Custom Post Type: Buildings (`bnb_building`) + - Address meta box with full address fields + - Contact meta box with phone, email, website + - Details meta box with rooms count, floors, year built, check-in/out times + - Custom admin columns (city, country, room count) + - Sortable columns and country dropdown +- Created Custom Post Type: Rooms (`bnb_room`) + - Building relationship via meta field + - Room details: number, floor, size, capacity, beds, bathrooms + - Room status with color-coded badges + - Image gallery with media library and drag-and-drop sorting + - Building filter dropdown in admin list + - Custom admin columns with building link +- Created Custom Taxonomy: Room Types (`bnb_room_type`) + - Hierarchical structure with parent/child support + - Base capacity and sort order meta fields + - Default terms with subtypes (Standard > Single/Double/Twin, etc.) +- Created Custom Taxonomy: Amenities (`bnb_amenity`) + - Non-hierarchical (tag-like) structure + - Dashicon selection for visual display + - Icon column in taxonomy list + - Default amenities: WiFi, Parking, Breakfast, etc. +- Updated Plugin class to register post types and taxonomies +- Enhanced admin assets for post type edit screens +- Added gallery JavaScript with media library integration +- Updated activation hook to register CPTs before flushing rewrites +- Updated version to 0.1.0 + +**Learnings:** + +- Taxonomies must be registered before post types that use them +- `show_in_menu => 'wp-bnb'` adds post types under the plugin's main menu +- Room-building relationship uses post meta, not hierarchical post types +- Gallery implementation uses `wp.media` frame with multiple selection +- Admin assets need conditional loading based on both hook suffix and post type +- Status badges use inline styles for color coding (avoiding extra CSS complexity) diff --git a/PLAN.md b/PLAN.md index c0a28d6..2e63a1f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -17,24 +17,24 @@ This document outlines the implementation plan for the WP BnB Management plugin. - [x] Basic CSS and JS assets - [x] Documentation (README, PLAN, CLAUDE) -### v0.1.0 - Core Data Structures +### v0.1.0 - Core Data Structures (Current) -- [ ] Custom Post Type: Buildings +- [x] Custom Post Type: Buildings - Meta fields: address, contact, description, images - Admin columns and filtering - - Gutenberg block for display + - Gutenberg block for display (planned for Phase 6) -- [ ] Custom Post Type: Rooms +- [x] Custom Post Type: Rooms - Meta fields: building reference, capacity, amenities, images - Relationship to Buildings (parent) - Admin columns with building filter - - Gutenberg block for display + - Gutenberg block for display (planned for Phase 6) -- [ ] Custom Taxonomy: Room Types +- [x] Custom Taxonomy: Room Types - Standard, Suite, Family, Accessible, etc. - Hierarchical structure -- [ ] Custom Taxonomy: Amenities +- [x] Custom Taxonomy: Amenities - WiFi, Parking, Breakfast, etc. - Non-hierarchical (tags) @@ -285,15 +285,15 @@ The plugin will provide extensive hooks for customization: ## Version Milestones -| Version | Focus | Target | -|---------|-------|--------| -| 0.0.1 | Initial setup | Complete | -| 0.1.0 | Data structures | TBD | -| 0.2.0 | Pricing | TBD | -| 0.3.0 | Bookings | TBD | -| 0.4.0 | Guests | TBD | -| 0.5.0 | Services | TBD | -| 0.6.0 | Frontend | TBD | -| 0.7.0 | CF7 Integration | TBD | -| 0.8.0 | Dashboard | TBD | -| 1.0.0 | Stable Release | TBD | +| Version | Focus | Target | +| ------- | --------------- | -------- | +| 0.0.1 | Initial setup | Complete | +| 0.1.0 | Data structures | Complete | +| 0.2.0 | Pricing | TBD | +| 0.3.0 | Bookings | TBD | +| 0.4.0 | Guests | TBD | +| 0.5.0 | Services | TBD | +| 0.6.0 | Frontend | TBD | +| 0.7.0 | CF7 Integration | TBD | +| 0.8.0 | Dashboard | TBD | +| 1.0.0 | Stable Release | TBD | diff --git a/assets/css/admin.css b/assets/css/admin.css index 4161b17..e2647ba 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -79,3 +79,115 @@ .submit .button { margin: 0; } + +/* Room Gallery */ +.bnb-gallery-container { + padding: 10px 0; +} + +.bnb-gallery-images { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 15px; +} + +.bnb-gallery-image { + position: relative; + width: 100px; + height: 100px; + border: 1px solid #c3c4c7; + border-radius: 4px; + overflow: hidden; + cursor: move; +} + +.bnb-gallery-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.bnb-gallery-image .bnb-remove-image { + position: absolute; + top: 2px; + right: 2px; + width: 20px; + height: 20px; + padding: 0; + border: none; + border-radius: 50%; + background: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 14px; + line-height: 18px; + text-align: center; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; +} + +.bnb-gallery-image:hover .bnb-remove-image { + opacity: 1; +} + +.bnb-gallery-image .bnb-remove-image:hover { + background: #d63638; +} + +/* Status Badge */ +.bnb-status-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 3px; + color: #fff; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +/* Room Details Meta Box */ +#bnb_room_details .form-table td label { + display: inline-block; + min-width: 100px; +} + +/* Building Details Meta Box */ +#bnb_building_details p { + margin: 10px 0; +} + +#bnb_building_details label { + display: block; + margin-bottom: 5px; + font-weight: 600; +} + +#bnb_building_details input { + width: 100%; +} + +/* Admin Columns */ +.column-city, +.column-country, +.column-room_number, +.column-capacity, +.column-status { + width: 100px; +} + +.column-building { + width: 150px; +} + +.column-rooms { + width: 80px; +} + +/* Dashicons in columns */ +.column-capacity .dashicons { + color: #646970; + font-size: 16px; + vertical-align: middle; + margin-right: 3px; +} diff --git a/assets/js/admin.js b/assets/js/admin.js index 4d97d53..17c565e 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -91,9 +91,102 @@ } } + /** + * Initialize room gallery functionality. + */ + function initRoomGallery() { + var $container = $('#bnb-room-gallery'); + var $addButton = $('#bnb-add-gallery-images'); + var $input = $('#bnb_room_gallery'); + var $imagesContainer = $container.find('.bnb-gallery-images'); + + if (!$addButton.length) { + return; + } + + // Media frame for selecting images. + var mediaFrame; + + // Add images button click. + $addButton.on('click', function(e) { + e.preventDefault(); + + // If frame exists, reopen it. + if (mediaFrame) { + mediaFrame.open(); + return; + } + + // Create media frame. + mediaFrame = wp.media({ + title: wpBnbAdmin.i18n.selectImages, + button: { + text: wpBnbAdmin.i18n.addToGallery + }, + multiple: true, + library: { + type: 'image' + } + }); + + // Handle selection. + mediaFrame.on('select', function() { + var selection = mediaFrame.state().get('selection'); + selection.each(function(attachment) { + var data = attachment.toJSON(); + var thumbnail = data.sizes.thumbnail ? data.sizes.thumbnail.url : data.url; + + // Check if already in gallery. + if ($imagesContainer.find('[data-id="' + data.id + '"]').length) { + return; + } + + // Add image to gallery. + var $image = $(''); + $imagesContainer.append($image); + }); + + updateGalleryInput(); + }); + + mediaFrame.open(); + }); + + // Remove image button click. + $imagesContainer.on('click', '.bnb-remove-image', function(e) { + e.preventDefault(); + $(this).closest('.bnb-gallery-image').remove(); + updateGalleryInput(); + }); + + // Make gallery sortable. + $imagesContainer.sortable({ + items: '.bnb-gallery-image', + cursor: 'move', + update: function() { + updateGalleryInput(); + } + }); + + /** + * Update the hidden input with gallery image IDs. + */ + function updateGalleryInput() { + var ids = []; + $imagesContainer.find('.bnb-gallery-image').each(function() { + ids.push($(this).data('id')); + }); + $input.val(ids.join(',')); + } + } + // Initialize on document ready. $(document).ready(function() { initLicenseManagement(); + initRoomGallery(); }); })(jQuery); diff --git a/src/Plugin.php b/src/Plugin.php index 4c33f28..e83773c 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -10,6 +10,10 @@ declare( strict_types=1 ); namespace Magdev\WpBnb; use Magdev\WpBnb\License\Manager as LicenseManager; +use Magdev\WpBnb\PostTypes\Building; +use Magdev\WpBnb\PostTypes\Room; +use Magdev\WpBnb\Taxonomies\Amenity; +use Magdev\WpBnb\Taxonomies\RoomType; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -67,6 +71,31 @@ final class Plugin { // Add plugin action links. add_filter( 'plugin_action_links_' . WP_BNB_BASENAME, array( $this, 'add_action_links' ) ); + + // Register custom post types and taxonomies. + $this->register_post_types(); + $this->register_taxonomies(); + } + + /** + * Register custom post types. + * + * @return void + */ + private function register_post_types(): void { + Building::init(); + Room::init(); + } + + /** + * Register custom taxonomies. + * + * @return void + */ + private function register_taxonomies(): void { + // Taxonomies must be registered before post types that use them. + Amenity::init(); + RoomType::init(); } /** @@ -129,8 +158,14 @@ final class Plugin { * @return void */ public function enqueue_admin_assets( string $hook_suffix ): void { - // Only load on plugin pages. - if ( strpos( $hook_suffix, 'wp-bnb' ) === false ) { + global $post_type; + + // 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 ), 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 ) ) { return; } @@ -141,10 +176,18 @@ final class Plugin { WP_BNB_VERSION ); + $script_deps = array( 'jquery' ); + + // Add media dependencies for room gallery. + if ( Room::POST_TYPE === $post_type && $is_edit_screen ) { + wp_enqueue_media(); + $script_deps[] = 'jquery-ui-sortable'; + } + wp_enqueue_script( 'wp-bnb-admin', WP_BNB_URL . 'assets/js/admin.js', - array( 'jquery' ), + $script_deps, WP_BNB_VERSION, true ); @@ -153,12 +196,16 @@ final class Plugin { 'wp-bnb-admin', 'wpBnbAdmin', array( - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ), - 'i18n' => array( - 'validating' => __( 'Validating...', 'wp-bnb' ), - 'activating' => __( 'Activating...', 'wp-bnb' ), - 'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + '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' ), ), ) ); diff --git a/src/PostTypes/Building.php b/src/PostTypes/Building.php new file mode 100644 index 0000000..2e542a5 --- /dev/null +++ b/src/PostTypes/Building.php @@ -0,0 +1,562 @@ + _x( 'Buildings', 'post type general name', 'wp-bnb' ), + 'singular_name' => _x( 'Building', 'post type singular name', 'wp-bnb' ), + 'menu_name' => _x( 'Buildings', 'admin menu', 'wp-bnb' ), + 'name_admin_bar' => _x( 'Building', 'add new on admin bar', 'wp-bnb' ), + 'add_new' => _x( 'Add New', 'building', 'wp-bnb' ), + 'add_new_item' => __( 'Add New Building', 'wp-bnb' ), + 'new_item' => __( 'New Building', 'wp-bnb' ), + 'edit_item' => __( 'Edit Building', 'wp-bnb' ), + 'view_item' => __( 'View Building', 'wp-bnb' ), + 'all_items' => __( 'Buildings', 'wp-bnb' ), + 'search_items' => __( 'Search Buildings', 'wp-bnb' ), + 'parent_item_colon' => __( 'Parent Buildings:', 'wp-bnb' ), + 'not_found' => __( 'No buildings found.', 'wp-bnb' ), + 'not_found_in_trash' => __( 'No buildings found in Trash.', 'wp-bnb' ), + 'featured_image' => __( 'Building Image', 'wp-bnb' ), + 'set_featured_image' => __( 'Set building image', 'wp-bnb' ), + 'remove_featured_image' => __( 'Remove building image', 'wp-bnb' ), + 'use_featured_image' => __( 'Use as building image', 'wp-bnb' ), + 'archives' => __( 'Building archives', 'wp-bnb' ), + 'insert_into_item' => __( 'Insert into building', 'wp-bnb' ), + 'uploaded_to_this_item' => __( 'Uploaded to this building', 'wp-bnb' ), + 'filter_items_list' => __( 'Filter buildings list', 'wp-bnb' ), + 'items_list_navigation' => __( 'Buildings list navigation', 'wp-bnb' ), + 'items_list' => __( 'Buildings list', 'wp-bnb' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => 'wp-bnb', + 'query_var' => true, + 'rewrite' => array( + 'slug' => 'building', + 'with_front' => false, + ), + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => null, + 'menu_icon' => 'dashicons-building', + 'supports' => array( + 'title', + 'editor', + 'thumbnail', + 'excerpt', + 'revisions', + ), + 'show_in_rest' => true, + 'rest_base' => 'buildings', + '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_building_address', + __( 'Address', 'wp-bnb' ), + array( self::class, 'render_address_meta_box' ), + self::POST_TYPE, + 'normal', + 'high' + ); + + add_meta_box( + 'bnb_building_contact', + __( 'Contact Information', 'wp-bnb' ), + array( self::class, 'render_contact_meta_box' ), + self::POST_TYPE, + 'normal', + 'high' + ); + + add_meta_box( + 'bnb_building_details', + __( 'Building Details', 'wp-bnb' ), + array( self::class, 'render_details_meta_box' ), + self::POST_TYPE, + 'side', + 'default' + ); + } + + /** + * Render address meta box. + * + * @param \WP_Post $post Current post object. + * @return void + */ + public static function render_address_meta_box( \WP_Post $post ): void { + wp_nonce_field( 'bnb_building_meta', 'bnb_building_meta_nonce' ); + + $street = get_post_meta( $post->ID, self::META_PREFIX . 'street', true ); + $street2 = get_post_meta( $post->ID, self::META_PREFIX . 'street2', true ); + $city = get_post_meta( $post->ID, self::META_PREFIX . 'city', true ); + $state = get_post_meta( $post->ID, self::META_PREFIX . 'state', true ); + $zip = get_post_meta( $post->ID, self::META_PREFIX . 'zip', true ); + $country = get_post_meta( $post->ID, self::META_PREFIX . 'country', true ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +

+
+ + + +
+ + + +
+ + + +
+ + + +
+ ID, self::META_PREFIX . 'phone', true ); + $email = get_post_meta( $post->ID, self::META_PREFIX . 'email', true ); + $website = get_post_meta( $post->ID, self::META_PREFIX . 'website', true ); + ?> + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ ID, self::META_PREFIX . 'total_rooms', true ); + $floors = get_post_meta( $post->ID, self::META_PREFIX . 'floors', true ); + $year_built = get_post_meta( $post->ID, self::META_PREFIX . 'year_built', true ); + $check_in = get_post_meta( $post->ID, self::META_PREFIX . 'check_in_time', true ); + $check_out = get_post_meta( $post->ID, self::META_PREFIX . 'check_out_time', true ); + ?> +

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+ $value ) { + $new_columns[ $key ] = $value; + if ( 'title' === $key ) { + $new_columns['city'] = __( 'City', 'wp-bnb' ); + $new_columns['country'] = __( 'Country', 'wp-bnb' ); + $new_columns['rooms'] = __( 'Rooms', 'wp-bnb' ); + } + } + 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 'city': + $city = get_post_meta( $post_id, self::META_PREFIX . 'city', true ); + echo esc_html( $city ?: '—' ); + break; + + case 'country': + $country = get_post_meta( $post_id, self::META_PREFIX . 'country', true ); + if ( $country ) { + $countries = self::get_countries(); + echo esc_html( $countries[ $country ] ?? $country ); + } else { + echo '—'; + } + break; + + case 'rooms': + $rooms = get_posts( + array( + 'post_type' => 'bnb_room', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'meta_query' => array( + array( + 'key' => '_bnb_room_building_id', + 'value' => $post_id, + ), + ), + ) + ); + $count = count( $rooms ); + if ( $count > 0 ) { + printf( + '%s', + esc_url( + admin_url( + 'edit.php?post_type=bnb_room&building_id=' . $post_id + ) + ), + esc_html( + sprintf( + /* translators: %d: Number of rooms */ + _n( '%d room', '%d rooms', $count, 'wp-bnb' ), + $count + ) + ) + ); + } else { + echo '—'; + } + break; + } + } + + /** + * Add sortable columns. + * + * @param array $columns Existing sortable columns. + * @return array + */ + public static function sortable_columns( array $columns ): array { + $columns['city'] = 'city'; + $columns['country'] = 'country'; + return $columns; + } + + /** + * 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 __( 'Enter building name', 'wp-bnb' ); + } + return $placeholder; + } + + /** + * Get list of countries. + * + * @return array + */ + public static function get_countries(): array { + return array( + 'CH' => __( 'Switzerland', 'wp-bnb' ), + 'DE' => __( 'Germany', 'wp-bnb' ), + 'AT' => __( 'Austria', 'wp-bnb' ), + 'FR' => __( 'France', 'wp-bnb' ), + 'IT' => __( 'Italy', 'wp-bnb' ), + 'LI' => __( 'Liechtenstein', 'wp-bnb' ), + 'NL' => __( 'Netherlands', 'wp-bnb' ), + 'BE' => __( 'Belgium', 'wp-bnb' ), + 'LU' => __( 'Luxembourg', 'wp-bnb' ), + 'GB' => __( 'United Kingdom', 'wp-bnb' ), + 'US' => __( 'United States', 'wp-bnb' ), + 'CA' => __( 'Canada', 'wp-bnb' ), + 'ES' => __( 'Spain', 'wp-bnb' ), + 'PT' => __( 'Portugal', 'wp-bnb' ), + ); + } + + /** + * Get formatted address. + * + * @param int $post_id Post ID. + * @return string + */ + public static function get_formatted_address( int $post_id ): string { + $street = get_post_meta( $post_id, self::META_PREFIX . 'street', true ); + $street2 = get_post_meta( $post_id, self::META_PREFIX . 'street2', true ); + $city = get_post_meta( $post_id, self::META_PREFIX . 'city', true ); + $state = get_post_meta( $post_id, self::META_PREFIX . 'state', true ); + $zip = get_post_meta( $post_id, self::META_PREFIX . 'zip', true ); + $country = get_post_meta( $post_id, self::META_PREFIX . 'country', true ); + + $parts = array(); + + if ( $street ) { + $parts[] = $street; + } + if ( $street2 ) { + $parts[] = $street2; + } + if ( $zip || $city ) { + $parts[] = trim( $zip . ' ' . $city ); + } + if ( $state ) { + $parts[] = $state; + } + if ( $country ) { + $countries = self::get_countries(); + $parts[] = $countries[ $country ] ?? $country; + } + + return implode( "\n", $parts ); + } +} diff --git a/src/PostTypes/Room.php b/src/PostTypes/Room.php new file mode 100644 index 0000000..6617163 --- /dev/null +++ b/src/PostTypes/Room.php @@ -0,0 +1,660 @@ + _x( 'Rooms', 'post type general name', 'wp-bnb' ), + 'singular_name' => _x( 'Room', 'post type singular name', 'wp-bnb' ), + 'menu_name' => _x( 'Rooms', 'admin menu', 'wp-bnb' ), + 'name_admin_bar' => _x( 'Room', 'add new on admin bar', 'wp-bnb' ), + 'add_new' => _x( 'Add New', 'room', 'wp-bnb' ), + 'add_new_item' => __( 'Add New Room', 'wp-bnb' ), + 'new_item' => __( 'New Room', 'wp-bnb' ), + 'edit_item' => __( 'Edit Room', 'wp-bnb' ), + 'view_item' => __( 'View Room', 'wp-bnb' ), + 'all_items' => __( 'Rooms', 'wp-bnb' ), + 'search_items' => __( 'Search Rooms', 'wp-bnb' ), + 'parent_item_colon' => __( 'Parent Rooms:', 'wp-bnb' ), + 'not_found' => __( 'No rooms found.', 'wp-bnb' ), + 'not_found_in_trash' => __( 'No rooms found in Trash.', 'wp-bnb' ), + 'featured_image' => __( 'Room Image', 'wp-bnb' ), + 'set_featured_image' => __( 'Set room image', 'wp-bnb' ), + 'remove_featured_image' => __( 'Remove room image', 'wp-bnb' ), + 'use_featured_image' => __( 'Use as room image', 'wp-bnb' ), + 'archives' => __( 'Room archives', 'wp-bnb' ), + 'insert_into_item' => __( 'Insert into room', 'wp-bnb' ), + 'uploaded_to_this_item' => __( 'Uploaded to this room', 'wp-bnb' ), + 'filter_items_list' => __( 'Filter rooms list', 'wp-bnb' ), + 'items_list_navigation' => __( 'Rooms list navigation', 'wp-bnb' ), + 'items_list' => __( 'Rooms list', 'wp-bnb' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => 'wp-bnb', + 'query_var' => true, + 'rewrite' => array( + 'slug' => 'room', + 'with_front' => false, + ), + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => null, + 'menu_icon' => 'dashicons-admin-home', + 'supports' => array( + 'title', + 'editor', + 'thumbnail', + 'excerpt', + 'revisions', + ), + 'show_in_rest' => true, + 'rest_base' => 'rooms', + 'rest_controller_class' => 'WP_REST_Posts_Controller', + 'taxonomies' => array( RoomType::TAXONOMY, Amenity::TAXONOMY ), + ); + + register_post_type( self::POST_TYPE, $args ); + } + + /** + * Add meta boxes. + * + * @return void + */ + public static function add_meta_boxes(): void { + add_meta_box( + 'bnb_room_building', + __( 'Building', 'wp-bnb' ), + array( self::class, 'render_building_meta_box' ), + self::POST_TYPE, + 'side', + 'high' + ); + + add_meta_box( + 'bnb_room_details', + __( 'Room Details', 'wp-bnb' ), + array( self::class, 'render_details_meta_box' ), + self::POST_TYPE, + 'normal', + 'high' + ); + + add_meta_box( + 'bnb_room_gallery', + __( 'Room Gallery', 'wp-bnb' ), + array( self::class, 'render_gallery_meta_box' ), + self::POST_TYPE, + 'normal', + 'default' + ); + } + + /** + * Render building selection meta box. + * + * @param \WP_Post $post Current post object. + * @return void + */ + public static function render_building_meta_box( \WP_Post $post ): void { + wp_nonce_field( 'bnb_room_meta', 'bnb_room_meta_nonce' ); + + $building_id = get_post_meta( $post->ID, self::META_PREFIX . 'building_id', true ); + $buildings = get_posts( + array( + 'post_type' => Building::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'title', + 'order' => 'ASC', + ) + ); + ?> +

+ +

+ + +

+ ' . esc_html__( 'Add a building', 'wp-bnb' ) . '' + ); + ?> +

+ + ID, self::META_PREFIX . 'room_number', true ); + $floor = get_post_meta( $post->ID, self::META_PREFIX . 'floor', true ); + $capacity = get_post_meta( $post->ID, self::META_PREFIX . 'capacity', true ); + $max_adults = get_post_meta( $post->ID, self::META_PREFIX . 'max_adults', true ); + $max_children = get_post_meta( $post->ID, self::META_PREFIX . 'max_children', true ); + $size = get_post_meta( $post->ID, self::META_PREFIX . 'size', true ); + $beds = get_post_meta( $post->ID, self::META_PREFIX . 'beds', true ); + $bathrooms = get_post_meta( $post->ID, self::META_PREFIX . 'bathrooms', true ); + $status = get_post_meta( $post->ID, self::META_PREFIX . 'status', true ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+
+ + + +
+ + + +
+ + + + +

+ + +

+ + +
+ + + +

+
+ + + +
+ + + +
+ ID, self::META_PREFIX . 'gallery', true ); + $gallery_ids = $gallery_ids ? explode( ',', $gallery_ids ) : array(); + ?> + +

+ $value ) { + if ( 'taxonomy-bnb_room_type' === $key ) { + continue; // Will add after title. + } + if ( 'taxonomy-bnb_amenity' === $key ) { + continue; // Will add after room type. + } + $new_columns[ $key ] = $value; + if ( 'title' === $key ) { + $new_columns['building'] = __( 'Building', 'wp-bnb' ); + $new_columns['room_number'] = __( 'Room #', 'wp-bnb' ); + $new_columns['taxonomy-bnb_room_type'] = __( 'Room Type', 'wp-bnb' ); + $new_columns['capacity'] = __( 'Capacity', 'wp-bnb' ); + $new_columns['status'] = __( 'Status', 'wp-bnb' ); + } + } + 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 'building': + $building_id = get_post_meta( $post_id, self::META_PREFIX . 'building_id', true ); + if ( $building_id ) { + $building = get_post( $building_id ); + if ( $building ) { + printf( + '%s', + esc_url( get_edit_post_link( $building_id ) ), + esc_html( $building->post_title ) + ); + } else { + echo '—'; + } + } else { + echo '—'; + } + break; + + case 'room_number': + $room_number = get_post_meta( $post_id, self::META_PREFIX . 'room_number', true ); + echo esc_html( $room_number ?: '—' ); + break; + + case 'capacity': + $capacity = get_post_meta( $post_id, self::META_PREFIX . 'capacity', true ); + if ( $capacity ) { + printf( + ' %s', + esc_html( $capacity ) + ); + } else { + echo '—'; + } + break; + + case 'status': + $status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'available'; + $statuses = self::get_room_statuses(); + $colors = self::get_status_colors(); + ?> + + + + Building::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'title', + 'order' => 'ASC', + ) + ); + + if ( empty( $buildings ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only. + $selected = isset( $_GET['building_id'] ) ? absint( $_GET['building_id'] ) : 0; + ?> + + is_main_query() ) { + return; + } + + if ( self::POST_TYPE !== $query->get( 'post_type' ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only. + if ( ! empty( $_GET['building_id'] ) ) { + $query->set( + 'meta_query', + array( + array( + 'key' => self::META_PREFIX . 'building_id', + 'value' => absint( $_GET['building_id'] ), + ), + ) + ); + } + } + + /** + * 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 __( 'Enter room name', 'wp-bnb' ); + } + return $placeholder; + } + + /** + * Get room status options. + * + * @return array + */ + public static function get_room_statuses(): array { + return array( + 'available' => __( 'Available', 'wp-bnb' ), + 'occupied' => __( 'Occupied', 'wp-bnb' ), + 'maintenance' => __( 'Maintenance', 'wp-bnb' ), + 'blocked' => __( 'Blocked', 'wp-bnb' ), + ); + } + + /** + * Get status color codes. + * + * @return array + */ + public static function get_status_colors(): array { + return array( + 'available' => '#00a32a', + 'occupied' => '#72aee6', + 'maintenance' => '#dba617', + 'blocked' => '#d63638', + ); + } + + /** + * Get building for a room. + * + * @param int $room_id Room post ID. + * @return \WP_Post|null + */ + public static function get_building( int $room_id ): ?\WP_Post { + $building_id = get_post_meta( $room_id, self::META_PREFIX . 'building_id', true ); + if ( ! $building_id ) { + return null; + } + return get_post( $building_id ); + } + + /** + * Get rooms for a building. + * + * @param int $building_id Building post ID. + * @return array<\WP_Post> + */ + public static function get_rooms_for_building( int $building_id ): array { + return get_posts( + array( + 'post_type' => self::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'meta_query' => array( + array( + 'key' => self::META_PREFIX . 'building_id', + 'value' => $building_id, + ), + ), + 'orderby' => 'meta_value', + 'meta_key' => self::META_PREFIX . 'room_number', + 'order' => 'ASC', + ) + ); + } +} diff --git a/src/Taxonomies/Amenity.php b/src/Taxonomies/Amenity.php new file mode 100644 index 0000000..bf5b0f1 --- /dev/null +++ b/src/Taxonomies/Amenity.php @@ -0,0 +1,242 @@ + _x( 'Amenities', 'taxonomy general name', 'wp-bnb' ), + 'singular_name' => _x( 'Amenity', 'taxonomy singular name', 'wp-bnb' ), + 'search_items' => __( 'Search Amenities', 'wp-bnb' ), + 'popular_items' => __( 'Popular Amenities', 'wp-bnb' ), + 'all_items' => __( 'All Amenities', 'wp-bnb' ), + 'parent_item' => null, + 'parent_item_colon' => null, + 'edit_item' => __( 'Edit Amenity', 'wp-bnb' ), + 'update_item' => __( 'Update Amenity', 'wp-bnb' ), + 'add_new_item' => __( 'Add New Amenity', 'wp-bnb' ), + 'new_item_name' => __( 'New Amenity Name', 'wp-bnb' ), + 'separate_items_with_commas' => __( 'Separate amenities with commas', 'wp-bnb' ), + 'add_or_remove_items' => __( 'Add or remove amenities', 'wp-bnb' ), + 'choose_from_most_used' => __( 'Choose from the most used amenities', 'wp-bnb' ), + 'not_found' => __( 'No amenities found.', 'wp-bnb' ), + 'menu_name' => __( 'Amenities', 'wp-bnb' ), + 'back_to_items' => __( '← Back to Amenities', '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' => true, + 'show_in_quick_edit' => true, + 'show_admin_column' => true, + 'rewrite' => array( + 'slug' => 'amenity', + '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_room' ), $args ); + } + + /** + * Add custom fields to the add term form. + * + * @return void + */ + public static function add_form_fields(): void { + ?> +
+ + +

+
+ term_id, 'amenity_icon', true ); + ?> + + + + + + +

+ + + $value ) { + $new_columns[ $key ] = $value; + if ( 'name' === $key ) { + $new_columns['icon'] = __( 'Icon', '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, 'amenity_icon', true ); + if ( $icon ) { + return ''; + } + return '—'; + } + return $content; + } + + /** + * Get available icon options. + * + * @return array + */ + public static function get_icon_options(): array { + return array( + '' => __( '— Select Icon —', 'wp-bnb' ), + 'wifi' => __( 'WiFi', 'wp-bnb' ), + 'car' => __( 'Parking', 'wp-bnb' ), + 'food' => __( 'Breakfast', 'wp-bnb' ), + 'palmtree' => __( 'Garden/Pool', 'wp-bnb' ), + 'pets' => __( 'Pet Friendly', 'wp-bnb' ), + 'universal-access' => __( 'Accessibility', 'wp-bnb' ), + 'tv' => __( 'Television', 'wp-bnb' ), + 'superhero-alt' => __( 'Air Conditioning', 'wp-bnb' ), + 'coffee' => __( 'Coffee/Tea', 'wp-bnb' ), + 'admin-home' => __( 'Kitchen', 'wp-bnb' ), + 'businessman' => __( 'Business Center', 'wp-bnb' ), + 'heart' => __( 'Spa/Wellness', 'wp-bnb' ), + 'groups' => __( 'Family Friendly', 'wp-bnb' ), + 'location-alt' => __( 'Central Location', 'wp-bnb' ), + 'building' => __( 'Elevator', 'wp-bnb' ), + 'store' => __( 'Minibar', 'wp-bnb' ), + 'admin-appearance' => __( 'Room Service', 'wp-bnb' ), + 'shield' => __( 'Safe', 'wp-bnb' ), + 'privacy' => __( 'Non-Smoking', 'wp-bnb' ), + ); + } + + /** + * Get default amenities to seed on activation. + * + * @return array + */ + public static function get_default_terms(): array { + return array( + __( 'WiFi', 'wp-bnb' ) => array( 'icon' => 'wifi' ), + __( 'Parking', 'wp-bnb' ) => array( 'icon' => 'car' ), + __( 'Breakfast Included', 'wp-bnb' ) => array( 'icon' => 'food' ), + __( 'Air Conditioning', 'wp-bnb' ) => array( 'icon' => 'superhero-alt' ), + __( 'Television', 'wp-bnb' ) => array( 'icon' => 'tv' ), + __( 'Pet Friendly', 'wp-bnb' ) => array( 'icon' => 'pets' ), + __( 'Wheelchair Accessible', 'wp-bnb' ) => array( 'icon' => 'universal-access' ), + __( 'Non-Smoking', 'wp-bnb' ) => array( 'icon' => 'privacy' ), + ); + } +} diff --git a/src/Taxonomies/RoomType.php b/src/Taxonomies/RoomType.php new file mode 100644 index 0000000..4e57f18 --- /dev/null +++ b/src/Taxonomies/RoomType.php @@ -0,0 +1,224 @@ + _x( 'Room Types', 'taxonomy general name', 'wp-bnb' ), + 'singular_name' => _x( 'Room Type', 'taxonomy singular name', 'wp-bnb' ), + 'search_items' => __( 'Search Room Types', 'wp-bnb' ), + 'all_items' => __( 'All Room Types', 'wp-bnb' ), + 'parent_item' => __( 'Parent Room Type', 'wp-bnb' ), + 'parent_item_colon' => __( 'Parent Room Type:', 'wp-bnb' ), + 'edit_item' => __( 'Edit Room Type', 'wp-bnb' ), + 'update_item' => __( 'Update Room Type', 'wp-bnb' ), + 'add_new_item' => __( 'Add New Room Type', 'wp-bnb' ), + 'new_item_name' => __( 'New Room Type Name', 'wp-bnb' ), + 'menu_name' => __( 'Room Types', 'wp-bnb' ), + 'back_to_items' => __( '← Back to Room Types', 'wp-bnb' ), + 'not_found' => __( 'No room types found.', 'wp-bnb' ), + ); + + $args = array( + 'labels' => $labels, + 'hierarchical' => true, // Hierarchical (like categories). + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'show_in_nav_menus' => true, + 'show_in_rest' => true, + 'show_in_quick_edit' => true, + 'show_admin_column' => true, + 'rewrite' => array( + 'slug' => 'room-type', + 'with_front' => false, + 'hierarchical' => true, + ), + '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_room' ), $args ); + } + + /** + * Add custom fields to the add term form. + * + * @return void + */ + public static function add_form_fields(): void { + ?> +
+ + +

+
+
+ + +

+
+ term_id, 'room_type_base_capacity', true ); + $sort_order = get_term_meta( $term->term_id, 'room_type_sort_order', true ); + ?> + + + + + + +

+ + + + + + + + +

+ + + }> + */ + public static function get_default_terms(): array { + return array( + __( 'Standard', 'wp-bnb' ) => array( + 'capacity' => 2, + 'order' => 10, + 'children' => array( + __( 'Single', 'wp-bnb' ) => array( + 'capacity' => 1, + 'order' => 11, + ), + __( 'Double', 'wp-bnb' ) => array( + 'capacity' => 2, + 'order' => 12, + ), + __( 'Twin', 'wp-bnb' ) => array( + 'capacity' => 2, + 'order' => 13, + ), + ), + ), + __( 'Superior', 'wp-bnb' ) => array( + 'capacity' => 2, + 'order' => 20, + ), + __( 'Suite', 'wp-bnb' ) => array( + 'capacity' => 2, + 'order' => 30, + 'children' => array( + __( 'Junior Suite', 'wp-bnb' ) => array( + 'capacity' => 2, + 'order' => 31, + ), + __( 'Executive Suite', 'wp-bnb' ) => array( + 'capacity' => 2, + 'order' => 32, + ), + ), + ), + __( 'Family', 'wp-bnb' ) => array( + 'capacity' => 4, + 'order' => 40, + ), + __( 'Accessible', 'wp-bnb' ) => array( + 'capacity' => 2, + 'order' => 50, + ), + __( 'Apartment', 'wp-bnb' ) => array( + 'capacity' => 4, + 'order' => 60, + ), + ); + } +} diff --git a/wp-bnb.php b/wp-bnb.php index 3e5dfdf..46d335c 100644 --- a/wp-bnb.php +++ b/wp-bnb.php @@ -3,7 +3,7 @@ * Plugin Name: WP BnB Management * Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb * Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests. - * Version: 0.0.1 + * Version: 0.1.0 * Requires at least: 6.0 * Requires PHP: 8.3 * Author: Marco Graetsch @@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Plugin version constant - MUST match Version in header above. -define( 'WP_BNB_VERSION', '0.0.1' ); +define( 'WP_BNB_VERSION', '0.1.0' ); // Plugin path constants. define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) ); @@ -155,6 +155,18 @@ function wp_bnb_activate(): void { ); } + // Load Composer autoloader for activation. + $autoloader = WP_BNB_PATH . 'vendor/autoload.php'; + if ( file_exists( $autoloader ) ) { + require_once $autoloader; + + // Register post types and taxonomies before flushing rewrite rules. + \Magdev\WpBnb\Taxonomies\Amenity::register(); + \Magdev\WpBnb\Taxonomies\RoomType::register(); + \Magdev\WpBnb\PostTypes\Building::register(); + \Magdev\WpBnb\PostTypes\Room::register(); + } + // Set default options. add_option( 'wp_bnb_version', WP_BNB_VERSION );