Add guest management and GDPR privacy compliance (v0.4.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m26s

- Create Guest CPT with personal info, address, ID/passport tracking
- Add guest-booking integration with AJAX search and linking
- Implement GDPR compliance via WordPress Privacy API (export/erasure)
- Update EmailNotifier to use Guest CPT data with new placeholders
- Add CSS styles for guest search, linked display, and privacy UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 14:59:43 +01:00
parent c66af8e299
commit aab3a4d1aa
9 changed files with 2858 additions and 71 deletions

View File

@@ -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 = '';
}
}
?>
<input type="hidden" id="bnb_booking_guest_id" name="bnb_booking_guest_id" value="<?php echo esc_attr( $guest_id ); ?>">
<?php if ( $linked_guest ) : ?>
<div id="bnb-linked-guest-info" class="bnb-linked-guest">
<p>
<span class="dashicons dashicons-admin-users"></span>
<strong><?php echo esc_html( $linked_guest->post_title ); ?></strong>
<a href="<?php echo esc_url( get_edit_post_link( $guest_id ) ); ?>" target="_blank" class="button button-small">
<?php esc_html_e( 'View Guest Profile', 'wp-bnb' ); ?>
</a>
<button type="button" id="bnb-unlink-guest" class="button button-small button-link-delete">
<?php esc_html_e( 'Unlink', 'wp-bnb' ); ?>
</button>
</p>
<?php if ( $guest_email ) : ?>
<p><small><?php echo esc_html( $guest_email ); ?></small></p>
<?php endif; ?>
</div>
<?php endif; ?>
<div id="bnb-guest-search-container" class="bnb-guest-search" <?php echo $linked_guest ? 'style="display:none;"' : ''; ?>>
<table class="form-table">
<tr>
<th scope="row">
<label for="bnb_booking_guest_search"><?php esc_html_e( 'Search Guest', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="text" id="bnb_booking_guest_search" class="regular-text"
placeholder="<?php esc_attr_e( 'Search by email...', 'wp-bnb' ); ?>">
<p class="description"><?php esc_html_e( 'Search for existing guest or enter details below.', 'wp-bnb' ); ?></p>
<div id="bnb-guest-search-results" class="bnb-guest-search-results" style="display:none;"></div>
</td>
</tr>
</table>
</div>
<div id="bnb-guest-fields-container" <?php echo $linked_guest ? 'style="display:none;"' : ''; ?>>
<table class="form-table">
<tr>
<th scope="row">
<label for="bnb_booking_guest_name"><?php esc_html_e( 'Guest Name', 'wp-bnb' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="bnb_booking_guest_name" name="bnb_booking_guest_name"
value="<?php echo esc_attr( $guest_name ); ?>" class="regular-text" <?php echo $linked_guest ? '' : 'required'; ?>>
</td>
</tr>
<tr>
<th scope="row">
<label for="bnb_booking_guest_email"><?php esc_html_e( 'Email', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="email" id="bnb_booking_guest_email" name="bnb_booking_guest_email"
value="<?php echo esc_attr( $guest_email ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row">
<label for="bnb_booking_guest_phone"><?php esc_html_e( 'Phone', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="tel" id="bnb_booking_guest_phone" name="bnb_booking_guest_phone"
value="<?php echo esc_attr( $guest_phone ); ?>" class="regular-text">
</td>
</tr>
</table>
</div>
<table class="form-table">
<tr>
<th scope="row">
<label for="bnb_booking_guest_name"><?php esc_html_e( 'Guest Name', 'wp-bnb' ); ?> <span class="required">*</span></label>
</th>
<td>
<input type="text" id="bnb_booking_guest_name" name="bnb_booking_guest_name"
value="<?php echo esc_attr( $guest_name ); ?>" class="regular-text" required>
</td>
</tr>
<tr>
<th scope="row">
<label for="bnb_booking_guest_email"><?php esc_html_e( 'Email', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="email" id="bnb_booking_guest_email" name="bnb_booking_guest_email"
value="<?php echo esc_attr( $guest_email ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row">
<label for="bnb_booking_guest_phone"><?php esc_html_e( 'Phone', 'wp-bnb' ); ?></label>
</th>
<td>
<input type="tel" id="bnb_booking_guest_phone" name="bnb_booking_guest_phone"
value="<?php echo esc_attr( $guest_phone ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Guests', 'wp-bnb' ); ?>
@@ -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(
'<a href="%s">%s</a>',
esc_url( get_edit_post_link( $guest_id ) ),
esc_html( $guest_name )
);
echo ' <span class="dashicons dashicons-admin-users" style="font-size: 14px; vertical-align: middle;" title="' . esc_attr__( 'Linked Guest', 'wp-bnb' ) . '"></span>';
} else {
echo esc_html( $guest_name );
}
if ( $guest_email ) {
echo '<br><small><a href="mailto:' . esc_attr( $guest_email ) . '">' . esc_html( $guest_email ) . '</a></small>';
}
@@ -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.
*