array( 'name' => ethebo__( 'Properties' ), 'singular_name' => ethebo__( 'Property' ), 'menu_name' => ethebo__( 'Accommodation' ), 'all_items' => ethebo__( 'All properties' ), 'add_new' => ethebo__( 'Add property' ), 'add_new_item' => ethebo__( 'Add new property' ), 'edit_item' => ethebo__( 'Edit property' ), 'new_item' => ethebo__( 'New property' ), 'view_item' => ethebo__( 'View property' ), 'search_items' => ethebo__( 'Search properties' ), 'not_found' => ethebo__( 'No properties found' ), 'not_found_in_trash' => ethebo__( 'No properties in trash' ), 'featured_image' => ethebo__( 'Main photo' ), 'set_featured_image' => ethebo__( 'Set main photo' ), ), 'public' => true, 'has_archive' => true, 'menu_icon' => 'dashicons-building', 'menu_position' => 25, 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), 'rewrite' => array( 'slug' => $slug ), 'show_in_rest' => true, ) ); register_taxonomy( self::TAX, self::PT, array( 'labels' => array( 'name' => ethebo__( 'Types' ), 'singular_name' => ethebo__( 'Type' ), 'add_new_item' => ethebo__( 'Add new type' ), 'edit_item' => ethebo__( 'Edit type' ), 'all_items' => ethebo__( 'All types' ), ), 'hierarchical' => true, 'show_admin_column' => true, 'show_in_rest' => true, 'rewrite' => array( 'slug' => $tax_slug ), ) ); } /** * The URL slugs depend on the language. Whenever they differ from the ones WordPress * last saved (language change, plugin update…), rebuild the rewrite rules once. */ public static function maybe_flush() { $slugs = implode( '|', ethebo_slugs() ); if ( get_option( 'ethebo_flush_rewrite' ) || get_option( 'ethebo_rewrite_slugs' ) !== $slugs ) { delete_option( 'ethebo_flush_rewrite' ); update_option( 'ethebo_rewrite_slugs', $slugs ); flush_rewrite_rules( false ); } } /** Old links (e.g. /akomodim/room-1/ after switching to English) redirect to the current slug. */ public static function redirect_old_slugs() { if ( ! is_404() ) { return; } $path = trim( (string) wp_parse_url( isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '', PHP_URL_PATH ), '/' ); $home = trim( (string) wp_parse_url( home_url(), PHP_URL_PATH ), '/' ); $path = $home && 0 === strpos( $path, $home . '/' ) ? substr( $path, strlen( $home ) + 1 ) : $path; $parts = explode( '/', $path, 2 ); list( $slug, $tax_slug ) = ethebo_slugs(); foreach ( ethebo_slugs( true ) as $old ) { $map = array( $old[0] => $slug, $old[1] => $tax_slug ); if ( isset( $map[ $parts[0] ] ) && $map[ $parts[0] ] !== $parts[0] ) { // "type"/"tipo" are common words, so only redirect taxonomy paths that really exist. if ( $old[1] === $parts[0] && ( empty( $parts[1] ) || ! get_term_by( 'slug', strtok( $parts[1], '/' ), self::TAX ) ) ) { continue; } wp_safe_redirect( user_trailingslashit( home_url( '/' . $map[ $parts[0] ] . ( isset( $parts[1] ) ? '/' . $parts[1] : '' ) ) ), 301 ); exit; } } } public static function meta_boxes() { add_meta_box( 'ethebo_details', ethebo__( 'Details & price' ), array( __CLASS__, 'details_box' ), self::PT, 'normal', 'high' ); add_meta_box( 'ethebo_gallery', ethebo__( 'Photo gallery' ), array( __CLASS__, 'gallery_box' ), self::PT, 'normal', 'high' ); add_meta_box( 'ethebo_sync', ethebo__( 'Sync with Booking.com / Airbnb' ), array( __CLASS__, 'sync_box' ), self::PT, 'normal', 'default' ); add_meta_box( 'ethebo_upcoming', ethebo__( 'Upcoming bookings' ), array( __CLASS__, 'upcoming_box' ), self::PT, 'side', 'default' ); } public static function details_box( $post ) { wp_nonce_field( 'ethebo_save_property', 'ethebo_nonce' ); $fields = array( // label, min, step, default 'price' => array( sprintf( ethebo__( 'Price per night (%s)' ), ethebo_opt( 'currency' ) ), 0, '0.01', '' ), 'max_guests' => array( ethebo__( 'Max guests' ), 1, '1', 2 ), 'min_nights' => array( ethebo__( 'Minimum nights' ), 1, '1', 1 ), 'bedrooms' => array( ethebo__( 'Bedrooms' ), 0, '1', '' ), 'bathrooms' => array( ethebo__( 'Bathrooms' ), 0, '1', '' ), ); echo '
'; foreach ( $fields as $key => $f ) { printf( '

', esc_attr( $key ), esc_html( $f[0] ), esc_attr( ethebo_meta( $post->ID, $key, $f[3] ) ), esc_attr( $f[1] ), esc_attr( $f[2] ) ); } echo '
'; echo '

' . esc_html( ethebo__( 'For a hotel: create each room as a separate property (e.g. "Hotel X – Double Room 101"), so each one has its own calendar.' ) ) . '

'; } /** Attachment IDs of the property gallery, in the chosen order. */ public static function gallery_ids( $post_id ) { return array_values( array_filter( array_map( 'absint', explode( ',', (string) get_post_meta( $post_id, '_ethebo_gallery', true ) ) ) ) ); } public static function gallery_box( $post ) { $ids = self::gallery_ids( $post->ID ); ?>

post_status; $feeds = array( 'booking' => array( 'label' => 'Booking.com', 'placeholder' => 'https://ical.booking.com/v1/export?t=...', 'help' => ethebo__( 'Booking.com Extranet → Rates & Availability → Sync calendars → choose the room → Export calendar → copy the link.' ), ), 'airbnb' => array( 'label' => 'Airbnb', 'placeholder' => 'https://www.airbnb.com/calendar/ical/123456.ics?s=...', 'help' => ethebo__( 'Airbnb → Listings → choose the listing → Availability → Connect calendars → Export calendar → copy the link.' ), ), ); ?>
1

$f ) : ?>

2

Import calendar (same place where you got the link in step 1).' ) ); ?>

$label ) : ?> ID, $ch ); ?>
ID ); ?>
ID ); ?>

Publish / Save once and the links for Booking.com and Airbnb will appear here.' ) ); ?>

3

ID ); ?>

check_out > $today; } ); ?>

$post->ID, 'upcoming' => true, 'limit' => 6 ) ); $rows = array_filter( $rows, function ( $r ) { return 'cancelled' !== $r->status; } ); if ( ! $rows ) { echo '

' . esc_html( ethebo__( 'No upcoming bookings.' ) ) . '

'; } else { echo ''; } printf( '

%s →

', esc_url( admin_url( 'edit.php?post_type=' . self::PT . '&page=ethebo-bookings&property=' . $post->ID ) ), esc_html( ethebo__( 'All bookings' ) ) ); } public static function save( $post_id, $post ) { if ( ! isset( $_POST['ethebo_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['ethebo_nonce'] ), 'ethebo_save_property' ) ) { return; } if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ! current_user_can( 'edit_post', $post_id ) ) { return; } $in = isset( $_POST['ethebo'] ) ? map_deep( wp_unslash( (array) $_POST['ethebo'] ), 'sanitize_textarea_field' ) : array(); update_post_meta( $post_id, '_ethebo_price', max( 0, round( (float) ( $in['price'] ?? 0 ), 2 ) ) ); update_post_meta( $post_id, '_ethebo_max_guests', max( 1, absint( $in['max_guests'] ?? 1 ) ) ); wp_cache_delete( 'ethebo_max_guests', 'ethebo' ); // The guests filter of the search reads the new maximum. update_post_meta( $post_id, '_ethebo_min_nights', max( 1, absint( $in['min_nights'] ?? 1 ) ) ); update_post_meta( $post_id, '_ethebo_bedrooms', '' === ( $in['bedrooms'] ?? '' ) ? '' : absint( $in['bedrooms'] ) ); update_post_meta( $post_id, '_ethebo_bathrooms', '' === ( $in['bathrooms'] ?? '' ) ? '' : absint( $in['bathrooms'] ) ); if ( isset( $in['gallery'] ) ) { $gallery = array_unique( array_filter( array_map( 'absint', explode( ',', (string) $in['gallery'] ) ), 'wp_attachment_is_image' ) ); update_post_meta( $post_id, '_ethebo_gallery', implode( ',', $gallery ) ); } $old_feeds = ETHEBO_iCal::feeds( $post_id ); update_post_meta( $post_id, '_ethebo_ical_booking', self::clean_url( $in['ical_booking'] ?? '' ) ); update_post_meta( $post_id, '_ethebo_ical_airbnb', self::clean_url( $in['ical_airbnb'] ?? '' ) ); $other = array_filter( array_map( array( __CLASS__, 'clean_url' ), preg_split( '/\s+/', (string) ( $in['ical_other'] ?? '' ) ) ) ); update_post_meta( $post_id, '_ethebo_ical_other', implode( "\n", array_unique( $other ) ) ); ETHEBO_iCal::token( $post_id ); // Links changed (or never synced): sync right away. $feeds = ETHEBO_iCal::feeds( $post_id ); if ( $feeds !== $old_feeds || ( $feeds && ! get_post_meta( $post_id, '_ethebo_last_sync', true ) ) ) { delete_transient( 'ethebo_sync_lock_' . $post_id ); ETHEBO_iCal::sync_property( $post_id ); } } /** Classic editor for properties: simpler, and meta boxes save/refresh immediately. */ public static function classic_editor( $use, $post_type ) { return self::PT === $post_type ? false : $use; } public static function clean_url( $url ) { $url = trim( (string) $url ); $url = preg_replace( '#^webcals?://#i', 'https://', $url ); return esc_url_raw( $url, array( 'http', 'https' ) ); } public static function on_delete( $post_id ) { if ( self::PT === get_post_type( $post_id ) ) { ETHEBO_Bookings::delete_for_property( $post_id ); } } public static function columns( $cols ) { $new = array(); foreach ( $cols as $key => $label ) { $new[ $key ] = $label; if ( 'title' === $key ) { $new['ethebo_price'] = ethebo__( 'Price / night' ); $new['ethebo_guests'] = ethebo__( 'Guests' ); $new['ethebo_sync'] = ethebo__( 'Sync' ); } } return $new; } public static function column( $col, $post_id ) { switch ( $col ) { case 'ethebo_price': echo esc_html( ethebo_price( ethebo_meta( $post_id, 'price', 0 ) ) ); break; case 'ethebo_guests': echo esc_html( ethebo_meta( $post_id, 'max_guests', '–' ) ); break; case 'ethebo_sync': $log = (array) get_post_meta( $post_id, '_ethebo_sync_log', true ); $log = array_filter( $log ); if ( ! $log ) { echo '—'; break; } foreach ( $log as $source => $e ) { printf( '%s ', $e['ok'] ? 'ok' : 'err', esc_attr( $e['ok'] ? 'OK' : $e['msg'] ), esc_html( ethebo_source_label( $source ) ) ); } break; } } }