diff --git a/CHANGELOG.md b/CHANGELOG.md index da29b72..24a626e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,63 @@ 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.4.0] - 2026-01-31 + +### Added + +- Guest Management System with dedicated CPT: + - Custom Post Type: Guests (`bnb_guest`) + - Personal information fields (name, email, phone, DOB, nationality) + - Address fields (street, city, postal code, country) + - Identification fields (ID type, number, expiry date) + - Guest status tracking (active, inactive, blocked) + - Internal notes and preferences + - GDPR consent tracking (marketing, data processing, consent date) + - Booking history display with statistics + - Helper methods: `get_by_email()`, `get_bookings()`, `get_booking_count()`, `get_total_spent()`, `get_full_name()`, `get_formatted_address()` +- Guest-Booking Integration: + - Guest search by email/name with AJAX autocomplete + - Link existing guests to bookings + - Create new guests from booking form + - Guest profile link in booking admin + - Automatic guest data sync when linked + - Backward compatibility for legacy bookings without guest_id +- GDPR/Privacy Compliance (`src/Privacy/Manager.php`): + - WordPress Privacy API integration + - Personal data exporter (guest profile + booking history) + - Personal data eraser with anonymization option + - Privacy policy content suggestion + - Support for WordPress Tools > Export/Erase Personal Data + - Guest anonymization (replaces PII with placeholder data) + - Booking anonymization for connected bookings +- Email Notifier Enhancements: + - Guest data retrieval from Guest CPT when available + - Fallback to booking meta for legacy bookings + - New placeholders: `{guest_first_name}`, `{guest_last_name}`, `{guest_full_address}` +- Admin UI Styles: + - Guest search container and results styling + - Linked guest display card + - Booking history table in Guest + - Consent status indicators + - Guest status badges + - Privacy action buttons + - Anonymized data display + +### Changed + +- Booking meta box updated with guest search/link functionality +- Plugin.php now initializes Guest CPT and Privacy Manager +- Admin JavaScript includes guest search with debounce +- Admin CSS extended with Guest and Privacy styles + +### Security + +- Guest email used as unique identifier for deduplication +- GDPR-compliant data export and erasure +- Consent tracking with timestamps +- Anonymization preserves booking records while removing PII +- AJAX endpoints secured with nonce verification + ## [0.3.0] - 2026-01-31 ### Added @@ -191,6 +248,7 @@ 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.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0 [0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.3.0 [0.2.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.2.0 [0.1.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.1.0 diff --git a/assets/css/admin.css b/assets/css/admin.css index d6f61d3..928ee63 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -688,3 +688,403 @@ gap: 10px; } } + +/* ========================================================================== + Guest Management Styles + ========================================================================== */ + +/* Guest Search Container */ +.bnb-guest-search-container { + margin-bottom: 15px; + padding: 12px; + background: #f6f7f7; + border: 1px solid #c3c4c7; + border-radius: 4px; +} + +.bnb-guest-search-container label { + display: block; + margin-bottom: 8px; + font-weight: 600; +} + +.bnb-guest-search-container input[type="text"] { + width: 100%; + max-width: 300px; +} + +/* Guest Search Results */ +.bnb-guest-results { + margin-top: 10px; + max-height: 200px; + overflow-y: auto; + border: 1px solid #c3c4c7; + border-radius: 4px; + background: #fff; +} + +.bnb-guest-result { + padding: 10px 12px; + border-bottom: 1px solid #f0f0f1; + cursor: pointer; + transition: background 0.15s ease; +} + +.bnb-guest-result:last-child { + border-bottom: none; +} + +.bnb-guest-result:hover { + background: #f0f6fc; +} + +.bnb-guest-result-name { + font-weight: 600; + color: #1d2327; +} + +.bnb-guest-result-email { + font-size: 12px; + color: #646970; + margin-top: 2px; +} + +.bnb-guest-result-meta { + font-size: 11px; + color: #a7aaad; + margin-top: 4px; +} + +.bnb-guest-no-results { + padding: 15px; + text-align: center; + color: #646970; + font-style: italic; +} + +.bnb-guest-create-new { + padding: 10px 12px; + background: #f0f6fc; + border-top: 1px solid #c3c4c7; + cursor: pointer; +} + +.bnb-guest-create-new:hover { + background: #d4e4f7; +} + +.bnb-guest-create-new .dashicons { + color: #2271b1; + margin-right: 5px; + vertical-align: text-bottom; +} + +/* Guest Linked Display */ +.bnb-guest-linked { + padding: 12px 15px; + background: #d4edda; + border: 1px solid #c3e6cb; + border-radius: 4px; + margin-bottom: 15px; +} + +.bnb-guest-linked-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 8px; +} + +.bnb-guest-linked-name { + font-size: 14px; + font-weight: 600; + color: #155724; +} + +.bnb-guest-linked-name .dashicons { + font-size: 16px; + width: 16px; + height: 16px; + margin-right: 5px; + vertical-align: text-bottom; +} + +.bnb-guest-linked-details { + font-size: 13px; + color: #155724; +} + +.bnb-guest-linked-details div { + margin-top: 4px; +} + +.bnb-guest-linked-actions { + display: flex; + gap: 8px; + margin-top: 10px; +} + +/* Guest Admin Columns */ +.column-email, +.column-phone, +.column-country, +.column-bookings { + width: 120px; +} + +.column-bookings a { + text-decoration: none; +} + +/* Guest Status Badges */ +.bnb-guest-status { + display: inline-block; + padding: 3px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +.bnb-guest-status-active { + background: #d4edda; + color: #155724; +} + +.bnb-guest-status-inactive { + background: #f6f7f7; + color: #646970; +} + +.bnb-guest-status-blocked { + background: #f8d7da; + color: #721c24; +} + +/* Booking History Table in Guest */ +.bnb-booking-history { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.bnb-booking-history th, +.bnb-booking-history td { + padding: 8px 10px; + text-align: left; + border-bottom: 1px solid #f0f0f1; +} + +.bnb-booking-history th { + background: #f6f7f7; + font-weight: 600; + color: #1d2327; +} + +.bnb-booking-history tr:hover td { + background: #f9f9f9; +} + +.bnb-booking-history-empty { + padding: 20px; + text-align: center; + color: #646970; + font-style: italic; +} + +.bnb-booking-stats { + display: flex; + gap: 20px; + padding: 12px 0; + border-bottom: 1px solid #c3c4c7; + margin-bottom: 15px; +} + +.bnb-booking-stat { + text-align: center; +} + +.bnb-booking-stat-value { + font-size: 20px; + font-weight: 600; + color: #135e96; +} + +.bnb-booking-stat-label { + font-size: 11px; + color: #646970; + text-transform: uppercase; +} + +/* ========================================================================== + GDPR / Privacy Styles + ========================================================================== */ + +/* Consent Status */ +.bnb-consent-status { + display: flex; + flex-direction: column; + gap: 8px; +} + +.bnb-consent-item { + display: flex; + align-items: center; + gap: 8px; +} + +.bnb-consent-item .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +.bnb-consent-granted { + color: #00a32a; +} + +.bnb-consent-not-granted { + color: #646970; +} + +.bnb-consent-date { + font-size: 11px; + color: #646970; + margin-left: 24px; +} + +/* Consent Checkboxes in Guest Form */ +.bnb-consent-checkbox { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 10px 0; +} + +.bnb-consent-checkbox input[type="checkbox"] { + margin-top: 3px; +} + +.bnb-consent-checkbox label { + font-weight: normal; + cursor: pointer; +} + +.bnb-consent-checkbox-description { + font-size: 12px; + color: #646970; + margin-left: 24px; + margin-top: 2px; +} + +/* Privacy Settings Section */ +.bnb-privacy-settings { + margin-top: 20px; +} + +.bnb-privacy-section { + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + margin-bottom: 20px; +} + +.bnb-privacy-section-header { + padding: 12px 15px; + background: #f6f7f7; + border-bottom: 1px solid #c3c4c7; + font-weight: 600; +} + +.bnb-privacy-section-content { + padding: 15px; +} + +.bnb-privacy-notice { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 15px; + background: #f0f6fc; + border-left: 4px solid #72aee6; + margin-bottom: 15px; +} + +.bnb-privacy-notice .dashicons { + color: #72aee6; + flex-shrink: 0; +} + +.bnb-privacy-notice p { + margin: 0; +} + +/* Privacy Actions in Guest Profile */ +.bnb-privacy-actions { + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #f0f0f1; +} + +.bnb-privacy-actions h4 { + margin: 0 0 10px 0; + font-size: 13px; +} + +.bnb-privacy-actions-buttons { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.bnb-privacy-action-button { + display: inline-flex; + align-items: center; + gap: 5px; +} + +.bnb-privacy-action-button .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +/* Anonymized Data Display */ +.bnb-anonymized { + font-style: italic; + color: #a7aaad; +} + +.bnb-anonymized-badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 8px; + background: #f0f0f1; + border-radius: 3px; + font-size: 11px; + color: #646970; +} + +.bnb-anonymized-badge .dashicons { + font-size: 14px; + width: 14px; + height: 14px; +} + +/* Data Retention Settings */ +.bnb-retention-settings { + display: flex; + align-items: center; + gap: 10px; +} + +.bnb-retention-settings input[type="number"] { + width: 80px; +} + +.bnb-retention-description { + font-size: 12px; + color: #646970; + margin-top: 5px; +} diff --git a/assets/js/admin.js b/assets/js/admin.js index dc0ef03..59018a5 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -577,6 +577,184 @@ }); } + /** + * Initialize guest search functionality for booking form. + */ + function initGuestSearch() { + var $searchInput = $('#bnb_booking_guest_search'); + var $searchResults = $('#bnb-guest-search-results'); + var $guestIdInput = $('#bnb_booking_guest_id'); + var $linkedGuestInfo = $('#bnb-linked-guest-info'); + var $searchContainer = $('#bnb-guest-search-container'); + var $fieldsContainer = $('#bnb-guest-fields-container'); + var $unlinkBtn = $('#bnb-unlink-guest'); + var $guestNameInput = $('#bnb_booking_guest_name'); + var $guestEmailInput = $('#bnb_booking_guest_email'); + var $guestPhoneInput = $('#bnb_booking_guest_phone'); + + // Exit if not on booking form. + if (!$searchInput.length) { + return; + } + + var searchTimer = null; + + /** + * Perform guest search via AJAX. + */ + function searchGuests() { + var query = $searchInput.val().trim(); + + if (query.length < 2) { + $searchResults.hide().empty(); + return; + } + + $searchResults.html('
' + wpBnbAdmin.i18n.searchingGuests + '
').show(); + + $.ajax({ + url: wpBnbAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'wp_bnb_search_guest', + nonce: wpBnbAdmin.nonce, + search: query + }, + success: function(response) { + if (response.success && response.data.guests.length > 0) { + var html = '
'; + + $.each(response.data.guests, function(i, guest) { + var isBlocked = guest.status === 'blocked'; + var statusClass = isBlocked ? 'bnb-guest-blocked' : ''; + var statusLabel = isBlocked ? ' ' + wpBnbAdmin.i18n.guestBlocked + '' : ''; + + html += '
'; + html += '
'; + html += '' + escapeHtml(guest.name) + '' + statusLabel + '
'; + html += '' + escapeHtml(guest.email || '') + ''; + if (guest.phone) { + html += ' (' + escapeHtml(guest.phone) + ')'; + } + html += '
'; + if (!isBlocked) { + html += ''; + } + html += '
'; + }); + + html += '
'; + $searchResults.html(html); + } else { + $searchResults.html('
' + wpBnbAdmin.i18n.noGuestsFound + '
'); + } + }, + error: function() { + $searchResults.html('
' + wpBnbAdmin.i18n.error + '
'); + } + }); + } + + /** + * Escape HTML entities. + * + * @param {string} text Text to escape. + * @return {string} Escaped text. + */ + function escapeHtml(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Select a guest from search results. + * + * @param {Object} guest Guest data. + */ + function selectGuest(guest) { + // Set hidden guest ID. + $guestIdInput.val(guest.id); + + // Populate guest fields (for display/fallback). + $guestNameInput.val(guest.name).prop('readonly', true); + $guestEmailInput.val(guest.email).prop('readonly', true); + $guestPhoneInput.val(guest.phone).prop('readonly', true); + + // Update linked guest display. + var infoHtml = '

'; + infoHtml += ' '; + infoHtml += '' + escapeHtml(guest.name) + ' '; + infoHtml += 'View Guest Profile '; + infoHtml += ''; + infoHtml += '

'; + if (guest.email) { + infoHtml += '

' + escapeHtml(guest.email) + '

'; + } + + $linkedGuestInfo.html(infoHtml).show(); + $searchContainer.hide(); + $fieldsContainer.hide(); + $searchResults.hide().empty(); + $searchInput.val(''); + + // Re-bind unlink button. + bindUnlinkButton(); + } + + /** + * Unlink guest from booking. + */ + function unlinkGuest() { + $guestIdInput.val(''); + $guestNameInput.val('').prop('readonly', false); + $guestEmailInput.val('').prop('readonly', false); + $guestPhoneInput.val('').prop('readonly', false); + + $linkedGuestInfo.hide(); + $searchContainer.show(); + $fieldsContainer.show(); + } + + /** + * Bind unlink button event. + */ + function bindUnlinkButton() { + $('#bnb-unlink-guest').off('click').on('click', function(e) { + e.preventDefault(); + unlinkGuest(); + }); + } + + // Search input with debounce. + $searchInput.on('input', function() { + if (searchTimer) { + clearTimeout(searchTimer); + } + searchTimer = setTimeout(searchGuests, 300); + }); + + // Select guest from results. + $searchResults.on('click', '.bnb-select-guest', function(e) { + e.preventDefault(); + var guest = $(this).closest('.bnb-guest-search-item').data('guest'); + if (guest) { + selectGuest(guest); + } + }); + + // Initial unlink button binding. + bindUnlinkButton(); + + // Close search results when clicking outside. + $(document).on('click', function(e) { + if (!$(e.target).closest('#bnb_booking_guest_search, #bnb-guest-search-results').length) { + $searchResults.hide(); + } + }); + } + // Initialize on document ready. $(document).ready(function() { initLicenseManagement(); @@ -586,6 +764,7 @@ initPricingMetaBox(); initBookingForm(); initCalendarPage(); + initGuestSearch(); }); })(jQuery); diff --git a/src/Booking/EmailNotifier.php b/src/Booking/EmailNotifier.php index 9aa571a..7fa0f87 100644 --- a/src/Booking/EmailNotifier.php +++ b/src/Booking/EmailNotifier.php @@ -12,6 +12,7 @@ declare( strict_types=1 ); namespace Magdev\WpBnb\Booking; use Magdev\WpBnb\PostTypes\Booking; +use Magdev\WpBnb\PostTypes\Guest; use Magdev\WpBnb\PostTypes\Room; use Magdev\WpBnb\Pricing\Calculator; @@ -203,8 +204,8 @@ final class EmailNotifier { * @return array Booking data. */ private static function get_booking_data( int $booking_id ): array { - $booking = get_post( $booking_id ); - $room = Booking::get_room( $booking_id ); + $booking = get_post( $booking_id ); + $room = Booking::get_room( $booking_id ); $building = Booking::get_building( $booking_id ); $check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true ); @@ -221,31 +222,84 @@ final class EmailNotifier { $statuses = Booking::get_booking_statuses(); + // Get guest data - prefer Guest CPT if linked, fallback to booking meta. + $guest_data = self::get_guest_data( $booking_id ); + return array( - 'booking_id' => $booking_id, - 'booking_reference' => $booking ? $booking->post_title : '', - 'guest_name' => get_post_meta( $booking_id, '_bnb_booking_guest_name', true ), - 'guest_email' => get_post_meta( $booking_id, '_bnb_booking_guest_email', true ), - 'guest_phone' => get_post_meta( $booking_id, '_bnb_booking_guest_phone', true ), - 'guest_notes' => get_post_meta( $booking_id, '_bnb_booking_guest_notes', true ), - 'adults' => $adults ?: 1, - 'children' => $children ?: 0, - 'room_name' => $room ? $room->post_title : '', - 'room_id' => $room ? $room->ID : 0, - 'building_name' => $building ? $building->post_title : '', - 'building_id' => $building ? $building->ID : 0, - 'check_in_date' => $check_in ? wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) : '', - 'check_out_date' => $check_out ? wp_date( get_option( 'date_format' ), strtotime( $check_out ) ) : '', - 'check_in_raw' => $check_in, - 'check_out_raw' => $check_out, - 'nights' => $nights, - 'total_price' => $price ? Calculator::formatPrice( (float) $price ) : '', - 'status' => $statuses[ $status ] ?? $status, - 'status_raw' => $status, - 'site_name' => get_bloginfo( 'name' ), - 'site_url' => home_url(), - 'admin_email' => get_option( 'admin_email' ), - 'booking_url' => admin_url( 'post.php?post=' . $booking_id . '&action=edit' ), + 'booking_id' => $booking_id, + 'booking_reference' => $booking ? $booking->post_title : '', + 'guest_name' => $guest_data['name'], + 'guest_first_name' => $guest_data['first_name'], + 'guest_last_name' => $guest_data['last_name'], + 'guest_email' => $guest_data['email'], + 'guest_phone' => $guest_data['phone'], + 'guest_notes' => $guest_data['notes'], + 'guest_full_address' => $guest_data['full_address'], + 'adults' => $adults ?: 1, + 'children' => $children ?: 0, + 'room_name' => $room ? $room->post_title : '', + 'room_id' => $room ? $room->ID : 0, + 'building_name' => $building ? $building->post_title : '', + 'building_id' => $building ? $building->ID : 0, + 'check_in_date' => $check_in ? wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) : '', + 'check_out_date' => $check_out ? wp_date( get_option( 'date_format' ), strtotime( $check_out ) ) : '', + 'check_in_raw' => $check_in, + 'check_out_raw' => $check_out, + 'nights' => $nights, + 'total_price' => $price ? Calculator::formatPrice( (float) $price ) : '', + 'status' => $statuses[ $status ] ?? $status, + 'status_raw' => $status, + 'site_name' => get_bloginfo( 'name' ), + 'site_url' => home_url(), + 'admin_email' => get_option( 'admin_email' ), + 'booking_url' => admin_url( 'post.php?post=' . $booking_id . '&action=edit' ), + ); + } + + /** + * Get guest data from Guest CPT or booking meta. + * + * @param int $booking_id Booking post ID. + * @return array Guest data with keys: name, first_name, last_name, email, phone, notes, full_address. + */ + private static function get_guest_data( int $booking_id ): array { + $guest_id = get_post_meta( $booking_id, '_bnb_booking_guest_id', true ); + + // Try to get data from Guest CPT. + if ( $guest_id ) { + $guest = get_post( $guest_id ); + if ( $guest && Guest::POST_TYPE === $guest->post_type ) { + $first_name = get_post_meta( $guest_id, '_bnb_guest_first_name', true ); + $last_name = get_post_meta( $guest_id, '_bnb_guest_last_name', true ); + + return array( + 'name' => Guest::get_full_name( $guest_id ), + 'first_name' => $first_name, + 'last_name' => $last_name, + 'email' => get_post_meta( $guest_id, '_bnb_guest_email', true ), + 'phone' => get_post_meta( $guest_id, '_bnb_guest_phone', true ), + 'notes' => get_post_meta( $guest_id, '_bnb_guest_notes', true ), + 'full_address' => Guest::get_formatted_address( $guest_id ), + ); + } + } + + // Fallback to booking meta (legacy bookings). + $guest_name = get_post_meta( $booking_id, '_bnb_booking_guest_name', true ); + + // Try to split name into first/last for legacy data. + $name_parts = explode( ' ', $guest_name, 2 ); + $first_name = $name_parts[0] ?? ''; + $last_name = $name_parts[1] ?? ''; + + return array( + 'name' => $guest_name, + 'first_name' => $first_name, + 'last_name' => $last_name, + 'email' => get_post_meta( $booking_id, '_bnb_booking_guest_email', true ), + 'phone' => get_post_meta( $booking_id, '_bnb_booking_guest_phone', true ), + 'notes' => get_post_meta( $booking_id, '_bnb_booking_guest_notes', true ), + 'full_address' => '', // Legacy bookings don't have full address. ); } diff --git a/src/Plugin.php b/src/Plugin.php index b1e2b07..4da2b73 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -16,7 +16,9 @@ use Magdev\WpBnb\Booking\EmailNotifier; use Magdev\WpBnb\License\Manager as LicenseManager; use Magdev\WpBnb\PostTypes\Booking; use Magdev\WpBnb\PostTypes\Building; +use Magdev\WpBnb\PostTypes\Guest; use Magdev\WpBnb\PostTypes\Room; +use Magdev\WpBnb\Privacy\Manager as PrivacyManager; use Magdev\WpBnb\Pricing\Season; use Magdev\WpBnb\Taxonomies\Amenity; use Magdev\WpBnb\Taxonomies\RoomType; @@ -92,6 +94,7 @@ final class Plugin { Building::init(); Room::init(); Booking::init(); + Guest::init(); } /** @@ -144,8 +147,12 @@ final class Plugin { // Initialize email notifier. EmailNotifier::init(); + // Initialize privacy manager for GDPR compliance. + PrivacyManager::init(); + // Register AJAX handlers. add_action( 'wp_ajax_wp_bnb_check_availability', array( $this, 'ajax_check_availability' ) ); + add_action( 'wp_ajax_wp_bnb_search_guest', array( $this, 'ajax_search_guest' ) ); } /** @@ -181,7 +188,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 ), true ); + $is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::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 ) ) { @@ -235,6 +242,10 @@ final class Plugin { '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' ), ), ) ); @@ -875,6 +886,68 @@ final class Plugin { wp_send_json_success( $result ); } + /** + * AJAX handler for searching guests by email. + * + * @return void + */ + public function ajax_search_guest(): void { + check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' ); + + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( + array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ) ) + ); + } + + $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; + + if ( strlen( $search ) < 2 ) { + wp_send_json_success( array( 'guests' => array() ) ); + } + + // Search by email or name. + $guests = get_posts( + array( + 'post_type' => Guest::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => 10, + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => '_bnb_guest_email', + 'value' => $search, + 'compare' => 'LIKE', + ), + array( + 'key' => '_bnb_guest_first_name', + 'value' => $search, + 'compare' => 'LIKE', + ), + array( + 'key' => '_bnb_guest_last_name', + 'value' => $search, + 'compare' => 'LIKE', + ), + ), + ) + ); + + $results = array(); + foreach ( $guests as $guest ) { + $status = get_post_meta( $guest->ID, '_bnb_guest_status', true ) ?: 'active'; + $results[] = array( + 'id' => $guest->ID, + 'name' => Guest::get_full_name( $guest->ID ), + 'email' => get_post_meta( $guest->ID, '_bnb_guest_email', true ), + 'phone' => get_post_meta( $guest->ID, '_bnb_guest_phone', true ), + 'status' => $status, + ); + } + + wp_send_json_success( array( 'guests' => $results ) ); + } + /** * Get Twig environment. * diff --git a/src/PostTypes/Booking.php b/src/PostTypes/Booking.php index 2b9ca1a..d138312 100644 --- a/src/PostTypes/Booking.php +++ b/src/PostTypes/Booking.php @@ -280,41 +280,97 @@ final class Booking { * @return void */ public static function render_guest_meta_box( \WP_Post $post ): void { + $guest_id = get_post_meta( $post->ID, self::META_PREFIX . 'guest_id', true ); $guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true ); $guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true ); $guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true ); $adults = get_post_meta( $post->ID, self::META_PREFIX . 'adults', true ); $children = get_post_meta( $post->ID, self::META_PREFIX . 'children', true ); $guest_notes = get_post_meta( $post->ID, self::META_PREFIX . 'guest_notes', true ); + + // If guest_id exists, get guest data from Guest CPT. + $linked_guest = null; + if ( $guest_id ) { + $linked_guest = get_post( $guest_id ); + if ( $linked_guest && Guest::POST_TYPE === $linked_guest->post_type ) { + $guest_name = Guest::get_full_name( $guest_id ); + $guest_email = get_post_meta( $guest_id, '_bnb_guest_email', true ); + $guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true ); + } else { + $linked_guest = null; + $guest_id = ''; + } + } ?> + + + +
+

+ + post_title ); ?> + + + + +

+ +

+ +
+ + + + +
> + + + + + + + + + + + + + +
+ + + > +
+ + + +
+ + + +
+
+ - - - - - - - - - - - -
- - - -
- - - -
- - - -
@@ -535,21 +591,48 @@ final class Booking { } } - // Guest text fields. - $guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' ); - foreach ( $guest_fields as $field ) { - $key = 'bnb_booking_' . $field; - if ( isset( $_POST[ $key ] ) ) { - $value = wp_unslash( $_POST[ $key ] ); - if ( 'guest_email' === $field ) { - $value = sanitize_email( $value ); - } elseif ( 'guest_notes' === $field ) { - $value = sanitize_textarea_field( $value ); - } else { - $value = sanitize_text_field( $value ); - } - update_post_meta( $post_id, self::META_PREFIX . $field, $value ); + // Guest ID (linked guest record). + $guest_id = isset( $_POST['bnb_booking_guest_id'] ) ? absint( $_POST['bnb_booking_guest_id'] ) : 0; + if ( $guest_id ) { + // Verify guest exists. + $guest = get_post( $guest_id ); + if ( $guest && Guest::POST_TYPE === $guest->post_type ) { + update_post_meta( $post_id, self::META_PREFIX . 'guest_id', $guest_id ); + // Sync guest data from Guest CPT for searching/display purposes. + update_post_meta( $post_id, self::META_PREFIX . 'guest_name', Guest::get_full_name( $guest_id ) ); + update_post_meta( $post_id, self::META_PREFIX . 'guest_email', get_post_meta( $guest_id, '_bnb_guest_email', true ) ); + update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', get_post_meta( $guest_id, '_bnb_guest_phone', true ) ); + } else { + delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' ); } + } else { + delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' ); + + // Guest text fields (only save if no guest_id). + $guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' ); + foreach ( $guest_fields as $field ) { + $key = 'bnb_booking_' . $field; + if ( isset( $_POST[ $key ] ) ) { + $value = wp_unslash( $_POST[ $key ] ); + if ( 'guest_email' === $field ) { + $value = sanitize_email( $value ); + } elseif ( 'guest_notes' === $field ) { + $value = sanitize_textarea_field( $value ); + } else { + $value = sanitize_text_field( $value ); + } + update_post_meta( $post_id, self::META_PREFIX . $field, $value ); + } + } + } + + // Guest notes are always saved (per-booking notes). + if ( isset( $_POST['bnb_booking_guest_notes'] ) ) { + update_post_meta( + $post_id, + self::META_PREFIX . 'guest_notes', + sanitize_textarea_field( wp_unslash( $_POST['bnb_booking_guest_notes'] ) ) + ); } // Guest counts. @@ -664,10 +747,21 @@ final class Booking { break; case 'guest': + $guest_id = get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true ); $guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true ); $guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true ); if ( $guest_name ) { - echo esc_html( $guest_name ); + if ( $guest_id ) { + // Linked guest - show link to guest profile. + printf( + '%s', + esc_url( get_edit_post_link( $guest_id ) ), + esc_html( $guest_name ) + ); + echo ' '; + } else { + echo esc_html( $guest_name ); + } if ( $guest_email ) { echo '
' . esc_html( $guest_email ) . ''; } @@ -1071,6 +1165,47 @@ final class Booking { return Room::get_building( $room->ID ); } + /** + * Get guest for a booking. + * + * Returns the linked Guest post if guest_id exists, or a stdClass object + * with guest data from booking meta for backward compatibility. + * + * @param int $booking_id Booking post ID. + * @return \WP_Post|\stdClass|null Guest post, virtual guest object, or null. + */ + public static function get_guest( int $booking_id ) { + $guest_id = get_post_meta( $booking_id, self::META_PREFIX . 'guest_id', true ); + + // If linked to Guest CPT, return the guest post. + if ( $guest_id ) { + $guest = get_post( $guest_id ); + if ( $guest && Guest::POST_TYPE === $guest->post_type ) { + return $guest; + } + } + + // Otherwise, create a virtual guest object from booking meta. + $guest_name = get_post_meta( $booking_id, self::META_PREFIX . 'guest_name', true ); + $guest_email = get_post_meta( $booking_id, self::META_PREFIX . 'guest_email', true ); + $guest_phone = get_post_meta( $booking_id, self::META_PREFIX . 'guest_phone', true ); + + if ( ! $guest_name && ! $guest_email ) { + return null; + } + + // Return virtual guest object for backward compatibility. + $virtual_guest = new \stdClass(); + $virtual_guest->ID = 0; + $virtual_guest->post_type = 'virtual_guest'; + $virtual_guest->post_title = $guest_name ?: ''; + $virtual_guest->name = $guest_name ?: ''; + $virtual_guest->email = $guest_email ?: ''; + $virtual_guest->phone = $guest_phone ?: ''; + + return $virtual_guest; + } + /** * Get all bookings for a room. * diff --git a/src/PostTypes/Guest.php b/src/PostTypes/Guest.php new file mode 100644 index 0000000..37b70e0 --- /dev/null +++ b/src/PostTypes/Guest.php @@ -0,0 +1,1086 @@ + _x( 'Guests', 'post type general name', 'wp-bnb' ), + 'singular_name' => _x( 'Guest', 'post type singular name', 'wp-bnb' ), + 'menu_name' => _x( 'Guests', 'admin menu', 'wp-bnb' ), + 'name_admin_bar' => _x( 'Guest', 'add new on admin bar', 'wp-bnb' ), + 'add_new' => _x( 'Add New', 'guest', 'wp-bnb' ), + 'add_new_item' => __( 'Add New Guest', 'wp-bnb' ), + 'new_item' => __( 'New Guest', 'wp-bnb' ), + 'edit_item' => __( 'Edit Guest', 'wp-bnb' ), + 'view_item' => __( 'View Guest', 'wp-bnb' ), + 'all_items' => __( 'Guests', 'wp-bnb' ), + 'search_items' => __( 'Search Guests', 'wp-bnb' ), + 'parent_item_colon' => __( 'Parent Guests:', 'wp-bnb' ), + 'not_found' => __( 'No guests found.', 'wp-bnb' ), + 'not_found_in_trash' => __( 'No guests found in Trash.', 'wp-bnb' ), + 'archives' => __( 'Guest archives', 'wp-bnb' ), + 'insert_into_item' => __( 'Insert into guest', 'wp-bnb' ), + 'uploaded_to_this_item' => __( 'Uploaded to this guest', 'wp-bnb' ), + 'filter_items_list' => __( 'Filter guests list', 'wp-bnb' ), + 'items_list_navigation' => __( 'Guests list navigation', 'wp-bnb' ), + 'items_list' => __( 'Guests list', 'wp-bnb' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => false, + 'publicly_queryable' => false, + 'show_ui' => true, + 'show_in_menu' => 'wp-bnb', + 'query_var' => false, + 'rewrite' => false, + 'capability_type' => 'post', + 'has_archive' => false, + 'hierarchical' => false, + 'menu_position' => null, + 'menu_icon' => 'dashicons-groups', + 'supports' => array( + 'title', + 'revisions', + ), + 'show_in_rest' => true, + 'rest_base' => 'guests', + '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_guest_personal', + __( 'Personal Information', 'wp-bnb' ), + array( self::class, 'render_personal_meta_box' ), + self::POST_TYPE, + 'normal', + 'high' + ); + + add_meta_box( + 'bnb_guest_address', + __( 'Address', 'wp-bnb' ), + array( self::class, 'render_address_meta_box' ), + self::POST_TYPE, + 'normal', + 'high' + ); + + add_meta_box( + 'bnb_guest_identification', + __( 'Identification', 'wp-bnb' ), + array( self::class, 'render_identification_meta_box' ), + self::POST_TYPE, + 'normal', + 'default' + ); + + add_meta_box( + 'bnb_guest_consent', + __( 'Consent & Privacy', 'wp-bnb' ), + array( self::class, 'render_consent_meta_box' ), + self::POST_TYPE, + 'side', + 'high' + ); + + add_meta_box( + 'bnb_guest_bookings', + __( 'Booking History', 'wp-bnb' ), + array( self::class, 'render_bookings_meta_box' ), + self::POST_TYPE, + 'normal', + 'default' + ); + + add_meta_box( + 'bnb_guest_status', + __( 'Status & Notes', 'wp-bnb' ), + array( self::class, 'render_status_meta_box' ), + self::POST_TYPE, + 'side', + 'default' + ); + } + + /** + * Render personal information meta box. + * + * @param \WP_Post $post Current post object. + * @return void + */ + public static function render_personal_meta_box( \WP_Post $post ): void { + wp_nonce_field( 'bnb_guest_meta', 'bnb_guest_meta_nonce' ); + + $first_name = get_post_meta( $post->ID, self::META_PREFIX . 'first_name', true ); + $last_name = get_post_meta( $post->ID, self::META_PREFIX . 'last_name', true ); + $email = get_post_meta( $post->ID, self::META_PREFIX . 'email', true ); + $phone = get_post_meta( $post->ID, self::META_PREFIX . 'phone', true ); + $date_of_birth = get_post_meta( $post->ID, self::META_PREFIX . 'date_of_birth', true ); + $nationality = get_post_meta( $post->ID, self::META_PREFIX . 'nationality', true ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +

+
+ + + +
+ + + +
+ + + +
+ ID, self::META_PREFIX . 'street', true ); + $city = get_post_meta( $post->ID, self::META_PREFIX . 'city', true ); + $postal_code = get_post_meta( $post->ID, self::META_PREFIX . 'postal_code', true ); + $country = get_post_meta( $post->ID, self::META_PREFIX . 'country', true ); + ?> + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ ID, self::META_PREFIX . 'id_type', true ); + $id_number = get_post_meta( $post->ID, self::META_PREFIX . 'id_number', true ); + $id_expiry = get_post_meta( $post->ID, self::META_PREFIX . 'id_expiry', true ); + ?> +

+ + +

+ + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ ID, self::META_PREFIX . 'consent_marketing', true ); + $consent_data = get_post_meta( $post->ID, self::META_PREFIX . 'consent_data', true ); + $consent_date = get_post_meta( $post->ID, self::META_PREFIX . 'consent_date', true ); + ?> +

+ +

+

+ +

+ +

+ +

+

+ +

+ + +

+
+ +

+ + ID ); + $total_spent = self::get_total_spent( $post->ID ); + $booking_count = count( $bookings ); + + if ( 0 === $booking_count ) { + echo '

' . esc_html__( 'No bookings found for this guest.', 'wp-bnb' ) . '

'; + return; + } + ?> +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + ID, '_bnb_booking_reference', true ); + $room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true ); + $room = get_post( $room_id ); + $check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true ); + $check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true ); + $price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true ); + $status = get_post_meta( $booking->ID, '_bnb_booking_status', true ); + $statuses = Booking::get_statuses(); + $colors = Booking::get_status_colors(); + ?> + + + + + + + + + +
+ + ID ); ?> + + post_title : '—' ); ?> + + + + + + + +
+ + 10 ) : ?> +

+ + + +

+ + ID, self::META_PREFIX . 'status', true ) ?: 'active'; + $notes = get_post_meta( $post->ID, self::META_PREFIX . 'notes', true ); + $preferences = get_post_meta( $post->ID, self::META_PREFIX . 'preferences', true ); + ?> +

+ + +

+ +

+ + + +

+ +

+ + + +

+ post_title !== $full_name ) { + remove_action( 'save_post_' . self::POST_TYPE, array( self::class, 'save_meta' ), 10 ); + wp_update_post( + array( + 'ID' => $post_id, + 'post_title' => $full_name, + ) + ); + add_action( 'save_post_' . self::POST_TYPE, array( self::class, 'save_meta' ), 10, 2 ); + } + } + } + + /** + * 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['email'] = __( 'Email', 'wp-bnb' ); + $new_columns['phone'] = __( 'Phone', 'wp-bnb' ); + $new_columns['country'] = __( 'Country', 'wp-bnb' ); + $new_columns['bookings'] = __( 'Bookings', 'wp-bnb' ); + $new_columns['status'] = __( 'Status', 'wp-bnb' ); + } + } + // Remove default 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 'email': + $email = get_post_meta( $post_id, self::META_PREFIX . 'email', true ); + if ( $email ) { + printf( + '%s', + esc_attr( $email ), + esc_html( $email ) + ); + } else { + echo '—'; + } + break; + + case 'phone': + $phone = get_post_meta( $post_id, self::META_PREFIX . 'phone', true ); + echo esc_html( $phone ?: '—' ); + break; + + case 'country': + $country = get_post_meta( $post_id, self::META_PREFIX . 'country', true ); + if ( $country ) { + $countries = Building::get_countries(); + echo esc_html( $countries[ $country ] ?? $country ); + } else { + echo '—'; + } + break; + + case 'bookings': + $count = self::get_booking_count( $post_id ); + if ( $count > 0 ) { + printf( + '%s', + esc_url( admin_url( 'edit.php?post_type=' . Booking::POST_TYPE . '&guest_id=' . $post_id ) ), + esc_html( + sprintf( + /* translators: %d: Number of bookings */ + _n( '%d booking', '%d bookings', $count, 'wp-bnb' ), + $count + ) + ) + ); + } else { + echo '—'; + } + break; + + case 'status': + $status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'active'; + $colors = self::get_status_colors(); + $statuses = self::get_statuses(); + printf( + '%s', + esc_attr( $colors[ $status ] ?? '#999' ), + esc_html( $statuses[ $status ] ?? $status ) + ); + break; + } + } + + /** + * Add sortable columns. + * + * @param array $columns Existing sortable columns. + * @return array + */ + public static function sortable_columns( array $columns ): array { + $columns['email'] = 'email'; + $columns['country'] = 'country'; + $columns['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['guest_status'] ) ? sanitize_text_field( wp_unslash( $_GET['guest_status'] ) ) : ''; + ?> + + + + is_main_query() ) { + return; + } + + if ( self::POST_TYPE !== $query->get( 'post_type' ) ) { + return; + } + + $meta_query = array(); + + // Status filter. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only. + if ( ! empty( $_GET['guest_status'] ) ) { + $meta_query[] = array( + 'key' => self::META_PREFIX . 'status', + 'value' => sanitize_text_field( wp_unslash( $_GET['guest_status'] ) ), + ); + } + + // Country filter. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only. + if ( ! empty( $_GET['guest_country'] ) ) { + $meta_query[] = array( + 'key' => self::META_PREFIX . 'country', + 'value' => sanitize_text_field( wp_unslash( $_GET['guest_country'] ) ), + ); + } + + if ( ! empty( $meta_query ) ) { + $meta_query['relation'] = 'AND'; + $query->set( 'meta_query', $meta_query ); + } + + // Handle orderby for custom columns. + $orderby = $query->get( 'orderby' ); + if ( 'email' === $orderby ) { + $query->set( 'meta_key', self::META_PREFIX . 'email' ); + $query->set( 'orderby', 'meta_value' ); + } elseif ( 'country' === $orderby ) { + $query->set( 'meta_key', self::META_PREFIX . 'country' ); + $query->set( 'orderby', 'meta_value' ); + } elseif ( 'status' === $orderby ) { + $query->set( 'meta_key', self::META_PREFIX . 'status' ); + $query->set( 'orderby', 'meta_value' ); + } + } + + /** + * Change title placeholder. + * + * @param string $placeholder Default placeholder. + * @param \WP_Post $post Current post. + * @return string + */ + public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string { + if ( self::POST_TYPE === $post->post_type ) { + return __( 'Guest name (auto-generated from first/last name)', 'wp-bnb' ); + } + return $placeholder; + } + + /** + * Get guest statuses. + * + * @return array + */ + public static function get_statuses(): array { + return array( + 'active' => __( 'Active', 'wp-bnb' ), + 'inactive' => __( 'Inactive', 'wp-bnb' ), + 'blocked' => __( 'Blocked', 'wp-bnb' ), + ); + } + + /** + * Get status colors. + * + * @return array + */ + public static function get_status_colors(): array { + return array( + 'active' => '#00a32a', + 'inactive' => '#72aee6', + 'blocked' => '#d63638', + ); + } + + /** + * Get ID types. + * + * @return array + */ + public static function get_id_types(): array { + return array( + 'passport' => __( 'Passport', 'wp-bnb' ), + 'id_card' => __( 'ID Card', 'wp-bnb' ), + 'drivers_license' => __( "Driver's License", 'wp-bnb' ), + 'other' => __( 'Other', 'wp-bnb' ), + ); + } + + /** + * Get guest by email. + * + * @param string $email Email address to search for. + * @return \WP_Post|null Guest post or null if not found. + */ + public static function get_by_email( string $email ): ?\WP_Post { + if ( empty( $email ) ) { + return null; + } + + $guests = get_posts( + array( + 'post_type' => self::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => 1, + 'meta_query' => array( + array( + 'key' => self::META_PREFIX . 'email', + 'value' => $email, + ), + ), + ) + ); + + return ! empty( $guests ) ? $guests[0] : null; + } + + /** + * Get all bookings for a guest. + * + * @param int $guest_id Guest post ID. + * @return array Array of booking posts. + */ + public static function get_bookings( int $guest_id ): array { + $email = get_post_meta( $guest_id, self::META_PREFIX . 'email', true ); + if ( empty( $email ) ) { + return array(); + } + + // First try to find bookings by guest_id (new way). + $bookings_by_id = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'meta_query' => array( + array( + 'key' => '_bnb_booking_guest_id', + 'value' => $guest_id, + ), + ), + ) + ); + + // Also find bookings by email (legacy way). + $bookings_by_email = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'meta_query' => array( + array( + 'key' => '_bnb_booking_guest_email', + 'value' => $email, + ), + ), + ) + ); + + // Merge and deduplicate. + $all_bookings = array_merge( $bookings_by_id, $bookings_by_email ); + $unique_ids = array(); + $result = array(); + + foreach ( $all_bookings as $booking ) { + if ( ! in_array( $booking->ID, $unique_ids, true ) ) { + $unique_ids[] = $booking->ID; + $result[] = $booking; + } + } + + // Sort by check-in date descending. + usort( + $result, + function ( $a, $b ) { + $date_a = get_post_meta( $a->ID, '_bnb_booking_check_in', true ); + $date_b = get_post_meta( $b->ID, '_bnb_booking_check_in', true ); + return strcmp( $date_b, $date_a ); + } + ); + + return $result; + } + + /** + * Get booking count for a guest. + * + * @param int $guest_id Guest post ID. + * @return int Number of bookings. + */ + public static function get_booking_count( int $guest_id ): int { + return count( self::get_bookings( $guest_id ) ); + } + + /** + * Get total spent by a guest. + * + * @param int $guest_id Guest post ID. + * @return float Total amount spent. + */ + public static function get_total_spent( int $guest_id ): float { + $bookings = self::get_bookings( $guest_id ); + $total = 0.0; + + foreach ( $bookings as $booking ) { + $status = get_post_meta( $booking->ID, '_bnb_booking_status', true ); + // Only count completed bookings (checked_out) or confirmed ones. + if ( in_array( $status, array( 'confirmed', 'checked_in', 'checked_out' ), true ) ) { + $price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true ); + $total += floatval( $price ); + } + } + + return $total; + } + + /** + * Get formatted address for a guest. + * + * @param int $guest_id Guest post ID. + * @return string Formatted address. + */ + public static function get_formatted_address( int $guest_id ): string { + $street = get_post_meta( $guest_id, self::META_PREFIX . 'street', true ); + $city = get_post_meta( $guest_id, self::META_PREFIX . 'city', true ); + $postal_code = get_post_meta( $guest_id, self::META_PREFIX . 'postal_code', true ); + $country = get_post_meta( $guest_id, self::META_PREFIX . 'country', true ); + + $parts = array(); + + if ( $street ) { + $parts[] = $street; + } + if ( $postal_code || $city ) { + $parts[] = trim( $postal_code . ' ' . $city ); + } + if ( $country ) { + $countries = Building::get_countries(); + $parts[] = $countries[ $country ] ?? $country; + } + + return implode( "\n", $parts ); + } + + /** + * Get guest's full name. + * + * @param int $guest_id Guest post ID. + * @return string Full name. + */ + public static function get_full_name( int $guest_id ): string { + $first_name = get_post_meta( $guest_id, self::META_PREFIX . 'first_name', true ); + $last_name = get_post_meta( $guest_id, self::META_PREFIX . 'last_name', true ); + + return trim( $first_name . ' ' . $last_name ); + } +} diff --git a/src/Privacy/Manager.php b/src/Privacy/Manager.php new file mode 100644 index 0000000..218bab1 --- /dev/null +++ b/src/Privacy/Manager.php @@ -0,0 +1,800 @@ +init_hooks(); + } + + /** + * Initialize hooks. + * + * @return void + */ + public static function init(): void { + self::get_instance(); + } + + /** + * Initialize WordPress hooks. + * + * @return void + */ + private function init_hooks(): void { + // Register personal data exporters. + add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporters' ) ); + + // Register personal data erasers. + add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_erasers' ) ); + + // Add privacy policy content suggestion. + add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) ); + } + + /** + * Register personal data exporters. + * + * @param array $exporters Existing exporters. + * @return array + */ + public function register_exporters( array $exporters ): array { + $exporters['wp-bnb-guest'] = array( + 'exporter_friendly_name' => __( 'WP BnB Guest Profile', 'wp-bnb' ), + 'callback' => array( $this, 'export_guest_data' ), + ); + + $exporters['wp-bnb-bookings'] = array( + 'exporter_friendly_name' => __( 'WP BnB Booking History', 'wp-bnb' ), + 'callback' => array( $this, 'export_booking_data' ), + ); + + return $exporters; + } + + /** + * Register personal data erasers. + * + * @param array $erasers Existing erasers. + * @return array + */ + public function register_erasers( array $erasers ): array { + $erasers['wp-bnb-guest'] = array( + 'eraser_friendly_name' => __( 'WP BnB Guest Profile', 'wp-bnb' ), + 'callback' => array( $this, 'erase_guest_data' ), + ); + + $erasers['wp-bnb-bookings'] = array( + 'eraser_friendly_name' => __( 'WP BnB Booking History', 'wp-bnb' ), + 'callback' => array( $this, 'erase_booking_data' ), + ); + + return $erasers; + } + + /** + * Export guest profile data. + * + * @param string $email Email address to export data for. + * @param int $page Page number for pagination. + * @return array Export data array. + */ + public function export_guest_data( string $email, int $page = 1 ): array { + $export_items = array(); + + // Find guest by email. + $guest = Guest::get_by_email( $email ); + + if ( $guest ) { + $data = array(); + + // Basic information. + $first_name = get_post_meta( $guest->ID, '_bnb_guest_first_name', true ); + $last_name = get_post_meta( $guest->ID, '_bnb_guest_last_name', true ); + + if ( $first_name ) { + $data[] = array( + 'name' => __( 'First Name', 'wp-bnb' ), + 'value' => $first_name, + ); + } + + if ( $last_name ) { + $data[] = array( + 'name' => __( 'Last Name', 'wp-bnb' ), + 'value' => $last_name, + ); + } + + $data[] = array( + 'name' => __( 'Email', 'wp-bnb' ), + 'value' => get_post_meta( $guest->ID, '_bnb_guest_email', true ), + ); + + $phone = get_post_meta( $guest->ID, '_bnb_guest_phone', true ); + if ( $phone ) { + $data[] = array( + 'name' => __( 'Phone', 'wp-bnb' ), + 'value' => $phone, + ); + } + + // Address. + $street = get_post_meta( $guest->ID, '_bnb_guest_street', true ); + if ( $street ) { + $data[] = array( + 'name' => __( 'Street Address', 'wp-bnb' ), + 'value' => $street, + ); + } + + $city = get_post_meta( $guest->ID, '_bnb_guest_city', true ); + if ( $city ) { + $data[] = array( + 'name' => __( 'City', 'wp-bnb' ), + 'value' => $city, + ); + } + + $postal_code = get_post_meta( $guest->ID, '_bnb_guest_postal_code', true ); + if ( $postal_code ) { + $data[] = array( + 'name' => __( 'Postal Code', 'wp-bnb' ), + 'value' => $postal_code, + ); + } + + $country = get_post_meta( $guest->ID, '_bnb_guest_country', true ); + if ( $country ) { + $data[] = array( + 'name' => __( 'Country', 'wp-bnb' ), + 'value' => $country, + ); + } + + // Personal details. + $nationality = get_post_meta( $guest->ID, '_bnb_guest_nationality', true ); + if ( $nationality ) { + $data[] = array( + 'name' => __( 'Nationality', 'wp-bnb' ), + 'value' => $nationality, + ); + } + + $date_of_birth = get_post_meta( $guest->ID, '_bnb_guest_date_of_birth', true ); + if ( $date_of_birth ) { + $data[] = array( + 'name' => __( 'Date of Birth', 'wp-bnb' ), + 'value' => $date_of_birth, + ); + } + + // ID information (sensitive). + $id_type = get_post_meta( $guest->ID, '_bnb_guest_id_type', true ); + if ( $id_type ) { + $data[] = array( + 'name' => __( 'ID Type', 'wp-bnb' ), + 'value' => $id_type, + ); + } + + $id_number = get_post_meta( $guest->ID, '_bnb_guest_id_number', true ); + if ( $id_number ) { + $data[] = array( + 'name' => __( 'ID Number', 'wp-bnb' ), + 'value' => $id_number, + ); + } + + $id_expiry = get_post_meta( $guest->ID, '_bnb_guest_id_expiry', true ); + if ( $id_expiry ) { + $data[] = array( + 'name' => __( 'ID Expiry Date', 'wp-bnb' ), + 'value' => $id_expiry, + ); + } + + // Consent information. + $consent_data = get_post_meta( $guest->ID, '_bnb_guest_consent_data', true ); + $data[] = array( + 'name' => __( 'Data Processing Consent', 'wp-bnb' ), + 'value' => $consent_data ? __( 'Yes', 'wp-bnb' ) : __( 'No', 'wp-bnb' ), + ); + + $consent_marketing = get_post_meta( $guest->ID, '_bnb_guest_consent_marketing', true ); + $data[] = array( + 'name' => __( 'Marketing Consent', 'wp-bnb' ), + 'value' => $consent_marketing ? __( 'Yes', 'wp-bnb' ) : __( 'No', 'wp-bnb' ), + ); + + $consent_date = get_post_meta( $guest->ID, '_bnb_guest_consent_date', true ); + if ( $consent_date ) { + $data[] = array( + 'name' => __( 'Consent Date', 'wp-bnb' ), + 'value' => $consent_date, + ); + } + + // Notes and preferences. + $preferences = get_post_meta( $guest->ID, '_bnb_guest_preferences', true ); + if ( $preferences ) { + $data[] = array( + 'name' => __( 'Guest Preferences', 'wp-bnb' ), + 'value' => $preferences, + ); + } + + if ( ! empty( $data ) ) { + $export_items[] = array( + 'group_id' => 'wp-bnb-guest', + 'group_label' => __( 'Guest Profile', 'wp-bnb' ), + 'group_description' => __( 'Your guest profile information stored by WP BnB.', 'wp-bnb' ), + 'item_id' => 'guest-' . $guest->ID, + 'data' => $data, + ); + } + } + + return array( + 'data' => $export_items, + 'done' => true, + ); + } + + /** + * Export booking history data. + * + * @param string $email Email address to export data for. + * @param int $page Page number for pagination. + * @return array Export data array. + */ + public function export_booking_data( string $email, int $page = 1 ): array { + $export_items = array(); + $per_page = 20; + $offset = ( $page - 1 ) * $per_page; + + // Find bookings by email (both direct and through guest_id). + $bookings = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'any', + 'posts_per_page' => $per_page, + 'offset' => $offset, + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => '_bnb_booking_guest_email', + 'value' => $email, + ), + ), + ) + ); + + // Also check via guest_id. + $guest = Guest::get_by_email( $email ); + if ( $guest ) { + $bookings_by_id = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'any', + 'posts_per_page' => $per_page, + 'meta_query' => array( + array( + 'key' => '_bnb_booking_guest_id', + 'value' => $guest->ID, + ), + ), + ) + ); + + // Merge and dedupe. + $existing_ids = wp_list_pluck( $bookings, 'ID' ); + foreach ( $bookings_by_id as $booking ) { + if ( ! in_array( $booking->ID, $existing_ids, true ) ) { + $bookings[] = $booking; + } + } + } + + foreach ( $bookings as $booking ) { + $data = array(); + + $reference = get_post_meta( $booking->ID, '_bnb_booking_reference', true ); + if ( ! $reference ) { + $reference = 'BNB-' . $booking->ID; + } + $data[] = array( + 'name' => __( 'Booking Reference', 'wp-bnb' ), + 'value' => $reference, + ); + + $room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true ); + if ( $room_id ) { + $room = get_post( $room_id ); + if ( $room ) { + $data[] = array( + 'name' => __( 'Room', 'wp-bnb' ), + 'value' => $room->post_title, + ); + } + } + + $check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true ); + if ( $check_in ) { + $data[] = array( + 'name' => __( 'Check-in Date', 'wp-bnb' ), + 'value' => $check_in, + ); + } + + $check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true ); + if ( $check_out ) { + $data[] = array( + 'name' => __( 'Check-out Date', 'wp-bnb' ), + 'value' => $check_out, + ); + } + + $status = get_post_meta( $booking->ID, '_bnb_booking_status', true ); + if ( $status ) { + $statuses = Booking::get_booking_statuses(); + $data[] = array( + 'name' => __( 'Status', 'wp-bnb' ), + 'value' => $statuses[ $status ] ?? $status, + ); + } + + $adults = get_post_meta( $booking->ID, '_bnb_booking_adults', true ); + if ( $adults ) { + $data[] = array( + 'name' => __( 'Adults', 'wp-bnb' ), + 'value' => $adults, + ); + } + + $children = get_post_meta( $booking->ID, '_bnb_booking_children', true ); + if ( $children ) { + $data[] = array( + 'name' => __( 'Children', 'wp-bnb' ), + 'value' => $children, + ); + } + + $price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true ); + if ( $price ) { + $currency = get_option( 'wp_bnb_currency', 'CHF' ); + $data[] = array( + 'name' => __( 'Total Price', 'wp-bnb' ), + 'value' => number_format( (float) $price, 2 ) . ' ' . $currency, + ); + } + + $guest_notes = get_post_meta( $booking->ID, '_bnb_booking_guest_notes', true ); + if ( $guest_notes ) { + $data[] = array( + 'name' => __( 'Guest Notes', 'wp-bnb' ), + 'value' => $guest_notes, + ); + } + + if ( ! empty( $data ) ) { + $export_items[] = array( + 'group_id' => 'wp-bnb-bookings', + 'group_label' => __( 'Booking History', 'wp-bnb' ), + 'group_description' => __( 'Your booking history with WP BnB.', 'wp-bnb' ), + 'item_id' => 'booking-' . $booking->ID, + 'data' => $data, + ); + } + } + + // Check if there are more bookings. + $total_bookings = $this->count_bookings_by_email( $email ); + $done = ( $offset + $per_page ) >= $total_bookings; + + return array( + 'data' => $export_items, + 'done' => $done, + ); + } + + /** + * Erase guest profile data. + * + * @param string $email Email address to erase data for. + * @param int $page Page number for pagination. + * @return array Erasure result array. + */ + public function erase_guest_data( string $email, int $page = 1 ): array { + $items_removed = 0; + $items_retained = 0; + $messages = array(); + + // Find guest by email. + $guest = Guest::get_by_email( $email ); + + if ( $guest ) { + // Check if guest has active bookings. + $active_bookings = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => 1, + 'meta_query' => array( + 'relation' => 'AND', + array( + 'relation' => 'OR', + array( + 'key' => '_bnb_booking_guest_id', + 'value' => $guest->ID, + ), + array( + 'key' => '_bnb_booking_guest_email', + 'value' => $email, + ), + ), + array( + 'key' => '_bnb_booking_status', + 'value' => array( 'pending', 'confirmed', 'checked_in' ), + 'compare' => 'IN', + ), + ), + ) + ); + + if ( ! empty( $active_bookings ) ) { + // Cannot delete - has active bookings. + $messages[] = __( 'Guest profile retained due to active bookings.', 'wp-bnb' ); + $items_retained = 1; + } else { + // Anonymize the guest profile instead of deleting. + $this->anonymize_guest( $guest->ID ); + $items_removed = 1; + $messages[] = __( 'Guest profile anonymized.', 'wp-bnb' ); + } + } + + return array( + 'items_removed' => $items_removed, + 'items_retained' => $items_retained, + 'messages' => $messages, + 'done' => true, + ); + } + + /** + * Erase booking data. + * + * @param string $email Email address to erase data for. + * @param int $page Page number for pagination. + * @return array Erasure result array. + */ + public function erase_booking_data( string $email, int $page = 1 ): array { + $items_removed = 0; + $items_retained = 0; + $messages = array(); + $per_page = 20; + + // Find completed bookings (can be anonymized). + $bookings = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'any', + 'posts_per_page' => $per_page, + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => '_bnb_booking_guest_email', + 'value' => $email, + ), + array( + 'key' => '_bnb_booking_status', + 'value' => array( 'checked_out', 'cancelled' ), + 'compare' => 'IN', + ), + ), + ) + ); + + // Also find by guest_id. + $guest = Guest::get_by_email( $email ); + if ( $guest ) { + $more_bookings = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'any', + 'posts_per_page' => $per_page, + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => '_bnb_booking_guest_id', + 'value' => $guest->ID, + ), + array( + 'key' => '_bnb_booking_status', + 'value' => array( 'checked_out', 'cancelled' ), + 'compare' => 'IN', + ), + ), + ) + ); + + $existing_ids = wp_list_pluck( $bookings, 'ID' ); + foreach ( $more_bookings as $booking ) { + if ( ! in_array( $booking->ID, $existing_ids, true ) ) { + $bookings[] = $booking; + } + } + } + + foreach ( $bookings as $booking ) { + $this->anonymize_booking( $booking->ID ); + ++$items_removed; + } + + // Check for active bookings that can't be erased. + $active_bookings = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => '_bnb_booking_guest_email', + 'value' => $email, + ), + array( + 'key' => '_bnb_booking_status', + 'value' => array( 'pending', 'confirmed', 'checked_in' ), + 'compare' => 'IN', + ), + ), + ) + ); + + $items_retained = count( $active_bookings ); + + if ( $items_retained > 0 ) { + $messages[] = sprintf( + /* translators: %d: Number of bookings */ + _n( + '%d booking retained due to active status.', + '%d bookings retained due to active status.', + $items_retained, + 'wp-bnb' + ), + $items_retained + ); + } + + if ( $items_removed > 0 ) { + $messages[] = sprintf( + /* translators: %d: Number of bookings */ + _n( + '%d booking anonymized.', + '%d bookings anonymized.', + $items_removed, + 'wp-bnb' + ), + $items_removed + ); + } + + return array( + 'items_removed' => $items_removed, + 'items_retained' => $items_retained, + 'messages' => $messages, + 'done' => true, + ); + } + + /** + * Anonymize a guest record. + * + * @param int $guest_id Guest post ID. + * @return bool True on success. + */ + public function anonymize_guest( int $guest_id ): bool { + $anonymized = __( '[Deleted]', 'wp-bnb' ); + + // Update post title. + wp_update_post( + array( + 'ID' => $guest_id, + 'post_title' => $anonymized, + ) + ); + + // Anonymize personal data. + update_post_meta( $guest_id, '_bnb_guest_first_name', $anonymized ); + update_post_meta( $guest_id, '_bnb_guest_last_name', '' ); + update_post_meta( $guest_id, '_bnb_guest_email', 'deleted-' . $guest_id . '@anonymized.local' ); + update_post_meta( $guest_id, '_bnb_guest_phone', '' ); + update_post_meta( $guest_id, '_bnb_guest_street', '' ); + update_post_meta( $guest_id, '_bnb_guest_city', '' ); + update_post_meta( $guest_id, '_bnb_guest_postal_code', '' ); + update_post_meta( $guest_id, '_bnb_guest_country', '' ); + update_post_meta( $guest_id, '_bnb_guest_nationality', '' ); + update_post_meta( $guest_id, '_bnb_guest_date_of_birth', '' ); + update_post_meta( $guest_id, '_bnb_guest_id_type', '' ); + update_post_meta( $guest_id, '_bnb_guest_id_number', '' ); + update_post_meta( $guest_id, '_bnb_guest_id_expiry', '' ); + update_post_meta( $guest_id, '_bnb_guest_preferences', '' ); + update_post_meta( $guest_id, '_bnb_guest_notes', '' ); + update_post_meta( $guest_id, '_bnb_guest_status', 'inactive' ); + + return true; + } + + /** + * Anonymize a booking record. + * + * @param int $booking_id Booking post ID. + * @return bool True on success. + */ + public function anonymize_booking( int $booking_id ): bool { + $anonymized = __( '[Deleted]', 'wp-bnb' ); + + // Remove guest reference. + delete_post_meta( $booking_id, '_bnb_booking_guest_id' ); + + // Anonymize guest data stored in booking. + update_post_meta( $booking_id, '_bnb_booking_guest_name', $anonymized ); + update_post_meta( $booking_id, '_bnb_booking_guest_email', '' ); + update_post_meta( $booking_id, '_bnb_booking_guest_phone', '' ); + update_post_meta( $booking_id, '_bnb_booking_guest_notes', '' ); + + return true; + } + + /** + * Count bookings by email. + * + * @param string $email Email address. + * @return int Count of bookings. + */ + private function count_bookings_by_email( string $email ): int { + $count = 0; + + // Direct email match. + $direct = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'any', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => '_bnb_booking_guest_email', + 'value' => $email, + ), + ), + ) + ); + + $count += count( $direct ); + + // Guest ID match. + $guest = Guest::get_by_email( $email ); + if ( $guest ) { + $by_guest_id = get_posts( + array( + 'post_type' => Booking::POST_TYPE, + 'post_status' => 'any', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'post__not_in' => $direct, + 'meta_query' => array( + array( + 'key' => '_bnb_booking_guest_id', + 'value' => $guest->ID, + ), + ), + ) + ); + $count += count( $by_guest_id ); + } + + return $count; + } + + /** + * Add privacy policy content suggestion. + * + * @return void + */ + public function add_privacy_policy_content(): void { + if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { + return; + } + + $content = sprintf( + '

%s

+

%s

+ +

%s

+

%s

+
    +
  • %s
  • +
  • %s
  • +
  • %s
  • +
  • %s
  • +
  • %s
  • +
+ +

%s

+

%s

+
    +
  • %s
  • +
  • %s
  • +
  • %s
  • +
+ +

%s

+

%s

+ +

%s

+

%s

', + __( 'Accommodation Booking', 'wp-bnb' ), + __( 'When you make a booking with us, we collect and process the following personal data to fulfill your reservation and comply with legal requirements.', 'wp-bnb' ), + __( 'What personal data we collect', 'wp-bnb' ), + __( 'We collect the following information when you make a booking:', 'wp-bnb' ), + __( 'Name and contact information (email, phone)', 'wp-bnb' ), + __( 'Address for billing and guest registration', 'wp-bnb' ), + __( 'Identity document information (as required by local regulations)', 'wp-bnb' ), + __( 'Booking details (dates, room preferences, special requests)', 'wp-bnb' ), + __( 'Payment information (processed securely by payment providers)', 'wp-bnb' ), + __( 'Why we collect this data', 'wp-bnb' ), + __( 'We use your personal data for the following purposes:', 'wp-bnb' ), + __( 'Processing and managing your booking', 'wp-bnb' ), + __( 'Communicating with you about your reservation', 'wp-bnb' ), + __( 'Complying with legal guest registration requirements', 'wp-bnb' ), + __( 'How long we retain your data', 'wp-bnb' ), + __( 'We retain your booking data for the period required by law for guest registration and accounting purposes, typically 10 years. After this period, your data will be anonymized or deleted.', 'wp-bnb' ), + __( 'Your rights', 'wp-bnb' ), + __( 'You have the right to access, correct, or request deletion of your personal data. To exercise these rights, please contact us using the information provided on this website. Note that some data may need to be retained for legal compliance purposes.', 'wp-bnb' ) + ); + + wp_add_privacy_policy_content( 'WP BnB', wp_kses_post( $content ) ); + } +} diff --git a/wp-bnb.php b/wp-bnb.php index ab3ab15..8f35c0c 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.3.0 + * Version: 0.4.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.3.0' ); +define( 'WP_BNB_VERSION', '0.4.0' ); // Plugin path constants. define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) ); @@ -165,6 +165,8 @@ function wp_bnb_activate(): void { \Magdev\WpBnb\Taxonomies\RoomType::register(); \Magdev\WpBnb\PostTypes\Building::register(); \Magdev\WpBnb\PostTypes\Room::register(); + \Magdev\WpBnb\PostTypes\Booking::register(); + \Magdev\WpBnb\PostTypes\Guest::register(); } // Set default options.