← All changes
|
includes/booking-appointment/booking-appointment__render.php
+274
-39
11.5
→
11.9
View file →
| @@ -193,8 +193,136 @@ | ||
| 193 | 193 | return $html; |
| 194 | 194 | } |
| 195 | 195 | |
| 196 | 196 | /** |
| 197 | + * Check whether the released frontend catalog presentation is available. | |
| 198 | + * | |
| 199 | + * The Appointment workflow remains fully usable with its legacy card grid when | |
| 200 | + * the 11.6 feature set is disabled. This keeps the release gate aligned with | |
| 201 | + * the Resource Selector catalog and its shortcode popup controls. | |
| 202 | + * | |
| 203 | + * @return bool True when Appointment catalog presentation may be used. | |
| 204 | + */ | |
| 205 | +function wpbc_booking_appointment_is_catalog_enabled() { | |
| 206 | + return function_exists( 'wpbc_is_11_6_features_enabled' ) && wpbc_is_11_6_features_enabled(); | |
| 207 | +} | |
| 208 | + | |
| 209 | +/** | |
| 210 | + * Prepare Appointment presentation values for the active feature state. | |
| 211 | + * | |
| 212 | + * Stored or manually authored 11.6 catalog values remain harmless when the | |
| 213 | + * feature is disabled: the compatibility renderer uses its established visible | |
| 214 | + * card content and ignores all new presentation toggles. | |
| 215 | + * | |
| 216 | + * @param array<string,mixed> $config Raw or normalized Appointment configuration. | |
| 217 | + * | |
| 218 | + * @return array<string,mixed> Normalized render configuration. | |
| 219 | + */ | |
| 220 | +function wpbc_booking_appointment_get_catalog_render_config( $config ) { | |
| 221 | + $config = wpbc_booking_appointment_normalize_config( $config ); | |
| 222 | + if ( wpbc_booking_appointment_is_catalog_enabled() ) { | |
| 223 | + return $config; | |
| 224 | + } | |
| 225 | + | |
| 226 | + foreach ( array( 'show_resource_image', 'show_resource_title', 'show_resource_description', 'show_resource_hierarchy', 'show_availability', 'show_starting_price' ) as $visible_config_key ) { | |
| 227 | + $config[ $visible_config_key ] = true; | |
| 228 | + } | |
| 229 | + | |
| 230 | + return $config; | |
| 231 | +} | |
| 232 | + | |
| 233 | +/** | |
| 234 | + * Return a grid track width that accounts for catalog gaps. | |
| 235 | + * | |
| 236 | + * @param string $item_width Normalized catalog item width. | |
| 237 | + * | |
| 238 | + * @return string Safe CSS grid track width. | |
| 239 | + */ | |
| 240 | +function wpbc_booking_appointment_get_catalog_track_width( $item_width ) { | |
| 241 | + if ( ! preg_match( '/^(\d+(?:\.\d+)?)%$/', $item_width, $matches ) ) { | |
| 242 | + return $item_width; | |
| 243 | + } | |
| 244 | + | |
| 245 | + $percentage_width = (float) $matches[1]; | |
| 246 | + $estimated_columns = max( 1, (int) floor( ( 100 + 0.0001 ) / $percentage_width ) ); | |
| 247 | + if ( $estimated_columns <= 1 ) { | |
| 248 | + return $item_width; | |
| 249 | + } | |
| 250 | + | |
| 251 | + $gap_adjustment = 10 * ( $estimated_columns - 1 ) / $estimated_columns; | |
| 252 | + $gap_adjustment = rtrim( rtrim( number_format( $gap_adjustment, 4, '.', '' ), '0' ), '.' ); | |
| 253 | + | |
| 254 | + return 'calc(' . $item_width . ' - ' . $gap_adjustment . 'px)'; | |
| 255 | +} | |
| 256 | + | |
| 257 | +/** | |
| 258 | + * Render the opening markup for a selectable Service or Provider catalog. | |
| 259 | + * | |
| 260 | + * @param array<string,mixed> $config Normalized Appointment configuration. | |
| 261 | + * @param string $catalog_type Either services or providers. | |
| 262 | + * @param int $item_count Number of authorized catalog items. | |
| 263 | + * | |
| 264 | + * @return string Escaped catalog opening markup, including an optional search field. | |
| 265 | + */ | |
| 266 | +function wpbc_booking_appointment_render_catalog_open( $config, $catalog_type, $item_count ) { | |
| 267 | + $catalog_type = 'providers' === $catalog_type ? 'providers' : 'services'; | |
| 268 | + $is_provider = 'providers' === $catalog_type; | |
| 269 | + $layout = 'list' === $config['catalog_layout'] ? 'list' : 'grid'; | |
| 270 | + $column_count = 'list' === $layout ? absint( $config['catalog_list_items_per_row'] ) : absint( $config['catalog_grid_items_per_row'] ); | |
| 271 | + $column_count = min( 12, $column_count ); | |
| 272 | + $item_width = wpbc_booking_appointment_normalize_css_width( $config['catalog_item_width'] ); | |
| 273 | + $catalog_class = 'wpbc_booking_appointment__catalog wpbc_booking_appointment__catalog--' . $layout; | |
| 274 | + $catalog_styles = array(); | |
| 275 | + | |
| 276 | + if ( empty( $config['show_resource_image'] ) ) { | |
| 277 | + $catalog_class .= ' wpbc_booking_appointment__catalog--without-images'; | |
| 278 | + } | |
| 279 | + if ( absint( $config['catalog_item_max_width'] ) > 0 ) { | |
| 280 | + $catalog_class .= ' wpbc_booking_appointment__catalog--bounded'; | |
| 281 | + $catalog_styles[] = '--wpbc-appointment-catalog-item-max-width:' . min( 1200, max( 280, absint( $config['catalog_item_max_width'] ) ) ) . 'px'; | |
| 282 | + } | |
| 283 | + if ( $column_count > 0 ) { | |
| 284 | + $catalog_class .= ' wpbc_booking_appointment__catalog--explicit-columns wpbc_booking_appointment__catalog--columns-' . $column_count; | |
| 285 | + $catalog_styles[] = '--wpbc-appointment-catalog-items-per-row:' . $column_count; | |
| 286 | + } elseif ( '' !== $item_width ) { | |
| 287 | + $catalog_class .= ' wpbc_booking_appointment__catalog--explicit-item-width'; | |
| 288 | + $catalog_styles[] = '--wpbc-appointment-catalog-item-width:' . $item_width; | |
| 289 | + $catalog_styles[] = '--wpbc-appointment-catalog-item-track-width:' . wpbc_booking_appointment_get_catalog_track_width( $item_width ); | |
| 290 | + } | |
| 291 | + | |
| 292 | + $catalog_style = empty( $catalog_styles ) ? '' : ' style="' . esc_attr( implode( ';', $catalog_styles ) . ';' ) . '"'; | |
| 293 | + $catalog_label = $is_provider ? __( 'Providers', 'booking' ) : __( 'Services', 'booking' ); | |
| 294 | + $filter_label = $is_provider ? __( 'Filter Providers', 'booking' ) : __( 'Filter Services', 'booking' ); | |
| 295 | + $search_label = $is_provider ? __( 'Search Providers', 'booking' ) : __( 'Search Services', 'booking' ); | |
| 296 | + $html = '<div class="' . esc_attr( $catalog_class ) . '" data-wpbc-appointment-catalog="" data-catalog-type="' . esc_attr( $catalog_type ) . '" data-layout="' . esc_attr( $layout ) . '"' . $catalog_style . '>'; | |
| 297 | + | |
| 298 | + if ( ! empty( $config['show_resource_filters'] ) && absint( $item_count ) > 1 ) { | |
| 299 | + $filter_id = wp_unique_id( 'wpbc_appointment_catalog_search_' ); | |
| 300 | + $html .= '<div class="wpbc_booking_appointment__catalog_filters" role="search" aria-label="' . esc_attr( $filter_label ) . '">'; | |
| 301 | + $html .= '<label class="wpbc_booking_appointment__catalog_search" for="' . esc_attr( $filter_id ) . '"><span class="screen-reader-text">' . esc_html( $search_label ) . '</span><input id="' . esc_attr( $filter_id ) . '" type="search" placeholder="' . esc_attr( $search_label ) . '" data-wpbc-appointment-catalog-search="" autocomplete="off"></label>'; | |
| 302 | + $html .= '</div>'; | |
| 303 | + } | |
| 304 | + | |
| 305 | + $html .= '<div class="wpbc_booking_appointment__choices wpbc_booking_appointment__catalog_items" role="radiogroup" aria-label="' . esc_attr( $catalog_label ) . '" data-wpbc-appointment-catalog-items="">'; | |
| 306 | + | |
| 307 | + return $html; | |
| 308 | +} | |
| 309 | + | |
| 310 | +/** | |
| 311 | + * Render the closing markup and accessible empty state for one catalog. | |
| 312 | + * | |
| 313 | + * @param string $catalog_type Either services or providers. | |
| 314 | + * | |
| 315 | + * @return string Escaped catalog closing markup. | |
| 316 | + */ | |
| 317 | +function wpbc_booking_appointment_render_catalog_close( $catalog_type ) { | |
| 318 | + $is_provider = 'providers' === $catalog_type; | |
| 319 | + $empty_label = $is_provider ? __( 'No Providers match your search.', 'booking' ) : __( 'No Services match your search.', 'booking' ); | |
| 320 | + | |
| 321 | + return '</div><p class="wpbc_booking_appointment__catalog_empty" data-wpbc-appointment-catalog-empty="" role="status" hidden>' . esc_html( $empty_label ) . '</p><p class="screen-reader-text" data-wpbc-appointment-catalog-status="" aria-live="polite"></p></div>'; | |
| 322 | +} | |
| 323 | + | |
| 324 | +/** | |
| 197 | 325 | * Render the Service choice stage. |
| 198 | 326 | * |
| 199 | 327 | * @param array<string,mixed> $catalog Public catalogue. |
| 200 | 328 | * @param string $config_token Signed AJAX configuration. |
| @@ -216,29 +344,52 @@ | ||
| 216 | 344 | |
| 217 | 345 | return wpbc_booking_appointment_render_notice( $message, 'empty' ); |
| 218 | 346 | } |
| 219 | 347 | |
| 348 | + $config = wpbc_booking_appointment_get_catalog_render_config( $config ); | |
| 349 | + $catalog_enabled = wpbc_booking_appointment_is_catalog_enabled(); | |
| 220 | 350 | $html = wpbc_booking_appointment_render_progress( 1, $config ); |
| 221 | 351 | $html .= wpbc_booking_appointment_render_screen_heading( $config, 1, __( 'Choose a Service', 'booking' ), __( 'Select what you would like to book.', 'booking' ) ); |
| 222 | 352 | $html .= '<form class="wpbc_booking_appointment__selection_form" method="get" action="' . esc_url( wpbc_booking_appointment_get_fallback_url( $config ) ) . '" data-appointment-stage="service">'; |
| 223 | - $html .= '<div class="wpbc_booking_appointment__choices" role="radiogroup" aria-label="' . esc_attr__( 'Services', 'booking' ) . '">'; | |
| 353 | + $html .= $catalog_enabled | |
| 354 | + ? wpbc_booking_appointment_render_catalog_open( $config, 'services', count( $catalog['services'] ) ) | |
| 355 | + : '<div class="wpbc_booking_appointment__choices" role="radiogroup" aria-label="' . esc_attr__( 'Services', 'booking' ) . '">'; | |
| 224 | 356 | foreach ( $catalog['services'] as $service ) { |
| 225 | - $service_id = absint( $service['service_id'] ); | |
| 226 | - $input_id = 'wpbc_appointment_service_' . $service_id . '_' . wp_rand( 1000, 9999 ); | |
| 227 | - $html .= '<label class="wpbc_booking_appointment__choice wpbc_booking_appointment__choice--service" for="' . esc_attr( $input_id ) . '" data-summary-service="' . esc_attr( $service['title'] ) . '" data-summary-duration="' . esc_attr( wpbc_booking_appointment_get_duration_summary( $service ) ) . '" data-summary-price="' . esc_attr( wpbc_booking_appointment_get_cost_summary( $service ) ) . '" data-summary-buffer-before="' . absint( isset( $service['buffer_before_minutes'] ) ? $service['buffer_before_minutes'] : 0 ) . '" data-summary-buffer-after="' . absint( isset( $service['buffer_after_minutes'] ) ? $service['buffer_after_minutes'] : 0 ) . '">'; | |
| 357 | + $service_id = absint( $service['service_id'] ); | |
| 358 | + $input_id = 'wpbc_appointment_service_' . $service_id . '_' . wp_rand( 1000, 9999 ); | |
| 359 | + $search_text = wp_strip_all_tags( (string) $service['title'] . ' ' . (string) $service['description'] ); | |
| 360 | + $choice_class = 'wpbc_booking_appointment__choice wpbc_booking_appointment__choice--service'; | |
| 361 | + $choice_attrs = ''; | |
| 362 | + if ( $catalog_enabled ) { | |
| 363 | + $choice_class .= ' wpbc_booking_appointment__catalog_card'; | |
| 364 | + if ( ! empty( $config['show_resource_image'] ) && empty( $service['picture_url'] ) ) { | |
| 365 | + $choice_class .= ' wpbc_booking_appointment__catalog_card--without-image'; | |
| 366 | + } | |
| 367 | + $choice_attrs = ' data-wpbc-appointment-catalog-card="" data-appointment-catalog-search="' . esc_attr( $search_text ) . '"'; | |
| 368 | + } | |
| 369 | + $html .= '<label class="' . esc_attr( $choice_class ) . '" for="' . esc_attr( $input_id ) . '"' . $choice_attrs . ' data-summary-service="' . esc_attr( $service['title'] ) . '" data-summary-duration="' . esc_attr( wpbc_booking_appointment_get_duration_summary( $service ) ) . '" data-summary-price="' . esc_attr( wpbc_booking_appointment_get_cost_summary( $service ) ) . '" data-summary-buffer-before="' . absint( isset( $service['buffer_before_minutes'] ) ? $service['buffer_before_minutes'] : 0 ) . '" data-summary-buffer-after="' . absint( isset( $service['buffer_after_minutes'] ) ? $service['buffer_after_minutes'] : 0 ) . '">'; | |
| 228 | 370 | $html .= '<input id="' . esc_attr( $input_id ) . '" type="radio" name="wpbc_appointment_service" value="' . $service_id . '" required>'; |
| 229 | - if ( ! empty( $service['picture_url'] ) ) { | |
| 371 | + if ( ! empty( $config['show_resource_image'] ) && ! empty( $service['picture_url'] ) ) { | |
| 230 | 372 | $html .= '<span class="wpbc_booking_appointment__service_image" aria-hidden="true"><img src="' . esc_url( $service['picture_url'] ) . '" alt="" loading="lazy" decoding="async"></span>'; |
| 231 | 373 | } |
| 232 | - $html .= '<span class="wpbc_booking_appointment__choice_body"><strong>' . esc_html( $service['title'] ) . '</strong>'; | |
| 233 | - $service_summary = wpbc_booking_appointment_join_summary_parts( array( wpbc_booking_appointment_get_duration_summary( $service ), wpbc_booking_appointment_get_cost_summary( $service ) ) ); | |
| 374 | + $html .= '<span class="wpbc_booking_appointment__choice_body">'; | |
| 375 | + $html .= ! empty( $config['show_resource_title'] ) | |
| 376 | + ? '<strong>' . esc_html( $service['title'] ) . '</strong>' | |
| 377 | + : '<span class="screen-reader-text">' . esc_html( $service['title'] ) . '</span>'; | |
| 378 | + $service_summary = wpbc_booking_appointment_join_summary_parts( | |
| 379 | + array( | |
| 380 | + wpbc_booking_appointment_get_duration_summary( $service ), | |
| 381 | + ! empty( $config['show_starting_price'] ) ? wpbc_booking_appointment_get_cost_summary( $service ) : '', | |
| 382 | + ) | |
| 383 | + ); | |
| 234 | 384 | $html .= '<span class="wpbc_booking_appointment__choice_meta">' . esc_html( $service_summary ) . '</span>'; |
| 235 | - if ( ! empty( $service['description'] ) ) { | |
| 385 | + if ( ! empty( $config['show_resource_description'] ) && ! empty( $service['description'] ) ) { | |
| 236 | 386 | $html .= '<span class="wpbc_booking_appointment__choice_description">' . esc_html( $service['description'] ) . '</span>'; |
| 237 | 387 | } |
| 238 | 388 | $html .= '</span><span class="wpbc_booking_appointment__choice_mark" aria-hidden="true"></span></label>'; |
| 239 | 389 | } |
| 240 | - $html .= '</div><input type="hidden" name="wpbc_appointment_config" value="' . esc_attr( $config_token ) . '">'; | |
| 390 | + $html .= ( $catalog_enabled ? wpbc_booking_appointment_render_catalog_close( 'services' ) : '</div>' ); | |
| 391 | + $html .= '<input type="hidden" name="wpbc_appointment_config" value="' . esc_attr( $config_token ) . '">'; | |
| 241 | 392 | $html .= '<div class="wpbc_booking_appointment__actions"><button type="submit" class="wpbc_button wpbc_button_primary wpbc_booking_appointment__continue">' . esc_html__( 'Continue', 'booking' ) . '</button></div></form>'; |
| 242 | 393 | |
| 243 | 394 | return $html; |
| 244 | 395 | } |
| @@ -248,8 +399,9 @@ | ||
| 248 | 399 | * |
| 249 | 400 | * @param array<string,mixed> $catalog Public catalogue. |
| 250 | 401 | * @param array<string,mixed> $service Selected Service. |
| 251 | 402 | * @param string $config_token Signed AJAX configuration. |
| 403 | + * @param array<string,mixed> $config Appointment configuration. | |
| 252 | 404 | * |
| 253 | 405 | * @return string Provider picker markup. |
| 254 | 406 | */ |
| 255 | 407 | function wpbc_booking_appointment_render_providers( $catalog, $service, $config_token, $config = array() ) { |
| @@ -257,14 +409,18 @@ | ||
| 257 | 409 | if ( empty( $providers ) ) { |
| 258 | 410 | return wpbc_booking_appointment_render_notice( __( 'No Providers are currently available for this Service.', 'booking' ), 'empty' ); |
| 259 | 411 | } |
| 260 | 412 | |
| 413 | + $config = wpbc_booking_appointment_get_catalog_render_config( $config ); | |
| 414 | + $catalog_enabled = wpbc_booking_appointment_is_catalog_enabled(); | |
| 261 | 415 | $html = wpbc_booking_appointment_render_progress( 2, $config ); |
| 262 | 416 | $html .= wpbc_booking_appointment_render_selected_service( $service ); |
| 263 | 417 | $html .= wpbc_booking_appointment_render_screen_heading( $config, 2, __( 'Choose a Provider', 'booking' ), __( 'Select who will provide this Service.', 'booking' ) ); |
| 264 | 418 | $html .= '<form class="wpbc_booking_appointment__selection_form" method="get" action="' . esc_url( wpbc_booking_appointment_get_fallback_url( $config ) ) . '" data-appointment-stage="provider">'; |
| 265 | 419 | $html .= '<input type="hidden" name="wpbc_appointment_service" value="' . absint( $service['service_id'] ) . '">'; |
| 266 | - $html .= '<div class="wpbc_booking_appointment__choices wpbc_booking_appointment__choices--providers" role="radiogroup" aria-label="' . esc_attr__( 'Providers', 'booking' ) . '">'; | |
| 420 | + $html .= $catalog_enabled | |
| 421 | + ? wpbc_booking_appointment_render_catalog_open( $config, 'providers', count( $providers ) ) | |
| 422 | + : '<div class="wpbc_booking_appointment__choices wpbc_booking_appointment__choices--providers" role="radiogroup" aria-label="' . esc_attr__( 'Providers', 'booking' ) . '">'; | |
| 267 | 423 | foreach ( $providers as $provider ) { |
| 268 | 424 | $provider_id = absint( $provider['provider_id'] ); |
| 269 | 425 | $provider_service = wpbc_booking_appointment_get_effective_service( $service, $provider_id ); |
| 270 | 426 | $input_id = 'wpbc_appointment_provider_' . $provider_id . '_' . wp_rand( 1000, 9999 ); |
| @@ -274,12 +430,12 @@ | ||
| 274 | 430 | } |
| 275 | 431 | $provider_details = wpbc_booking_appointment_join_summary_parts( |
| 276 | 432 | array( |
| 277 | 433 | wpbc_booking_appointment_format_duration( $provider_service['duration_minutes'] ), |
| 278 | - wpbc_booking_appointment_format_service_cost_text( $provider_service['base_cost'], $provider_id ), | |
| 434 | + ! empty( $config['show_starting_price'] ) ? wpbc_booking_appointment_format_service_cost_text( $provider_service['base_cost'], $provider_id ) : '', | |
| 279 | 435 | ) |
| 280 | 436 | ); |
| 281 | - if ( isset( $provider['has_weekly_availability'] ) && ! $provider['has_weekly_availability'] ) { | |
| 437 | + if ( ! empty( $config['show_availability'] ) && isset( $provider['has_weekly_availability'] ) && ! $provider['has_weekly_availability'] ) { | |
| 282 | 438 | $provider_details = wpbc_booking_appointment_join_summary_parts( |
| 283 | 439 | array( |
| 284 | 440 | $provider_details, |
| 285 | 441 | __( 'No weekly availability configured', 'booking' ), |
| @@ -285,25 +441,43 @@ | ||
| 285 | 441 | __( 'No weekly availability configured', 'booking' ), |
| 286 | 442 | ) |
| 287 | 443 | ); |
| 288 | 444 | } |
| 289 | - /* translators: %s: effective Service duration and optional price for this Provider. */ | |
| 290 | - $provider_meta = sprintf( __( 'Offers this Service · %s', 'booking' ), $provider_details ); | |
| 291 | - $html .= '<label class="wpbc_booking_appointment__choice wpbc_booking_appointment__choice--provider" for="' . esc_attr( $input_id ) . '" data-summary-provider="' . esc_attr( $provider['title'] ) . '" data-summary-duration="' . esc_attr( wpbc_booking_appointment_format_duration( $provider_service['duration_minutes'] ) ) . '" data-summary-price="' . esc_attr( wpbc_booking_appointment_format_service_cost_text( $provider_service['base_cost'], $provider_id ) ) . '" data-summary-buffer-before="' . absint( isset( $provider_service['buffer_before_minutes'] ) ? $provider_service['buffer_before_minutes'] : 0 ) . '" data-summary-buffer-after="' . absint( isset( $provider_service['buffer_after_minutes'] ) ? $provider_service['buffer_after_minutes'] : 0 ) . '">'; | |
| 445 | + if ( ! empty( $config['show_resource_hierarchy'] ) ) { | |
| 446 | + /* translators: %s: effective Service duration, optional price, and availability for this Provider. */ | |
| 447 | + $provider_meta = sprintf( __( 'Offers this Service · %s', 'booking' ), $provider_details ); | |
| 448 | + } else { | |
| 449 | + $provider_meta = $provider_details; | |
| 450 | + } | |
| 451 | + $search_text = wp_strip_all_tags( (string) $provider['title'] . ' ' . (string) $service['title'] . ' ' . $provider_meta ); | |
| 452 | + $choice_class = 'wpbc_booking_appointment__choice wpbc_booking_appointment__choice--provider'; | |
| 453 | + $choice_attrs = ''; | |
| 454 | + if ( $catalog_enabled ) { | |
| 455 | + $choice_class .= ' wpbc_booking_appointment__catalog_card'; | |
| 456 | + $choice_attrs = ' data-wpbc-appointment-catalog-card="" data-appointment-catalog-search="' . esc_attr( $search_text ) . '"'; | |
| 457 | + } | |
| 458 | + $html .= '<label class="' . esc_attr( $choice_class ) . '" for="' . esc_attr( $input_id ) . '"' . $choice_attrs . ' data-summary-provider="' . esc_attr( $provider['title'] ) . '" data-summary-duration="' . esc_attr( wpbc_booking_appointment_format_duration( $provider_service['duration_minutes'] ) ) . '" data-summary-price="' . esc_attr( wpbc_booking_appointment_format_service_cost_text( $provider_service['base_cost'], $provider_id ) ) . '" data-summary-buffer-before="' . absint( isset( $provider_service['buffer_before_minutes'] ) ? $provider_service['buffer_before_minutes'] : 0 ) . '" data-summary-buffer-after="' . absint( isset( $provider_service['buffer_after_minutes'] ) ? $provider_service['buffer_after_minutes'] : 0 ) . '">'; | |
| 292 | 459 | $html .= '<input id="' . esc_attr( $input_id ) . '" type="radio" name="wpbc_appointment_provider" value="' . $provider_id . '" required>'; |
| 293 | - $html .= '<span class="wpbc_booking_appointment__provider_avatar" aria-hidden="true">'; | |
| 294 | - if ( ! empty( $provider['image_url'] ) ) { | |
| 295 | - $html .= '<img src="' . esc_url( $provider['image_url'] ) . '" alt="" loading="lazy" decoding="async">'; | |
| 296 | - } else { | |
| 297 | - $initials = $initials ? $initials : 'P'; | |
| 298 | - $initials = function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $initials ) : strtoupper( $initials ); | |
| 299 | - $html .= esc_html( $initials ); | |
| 460 | + if ( ! empty( $config['show_resource_image'] ) ) { | |
| 461 | + $html .= '<span class="wpbc_booking_appointment__provider_avatar" aria-hidden="true">'; | |
| 462 | + if ( ! empty( $provider['image_url'] ) ) { | |
| 463 | + $html .= '<img src="' . esc_url( $provider['image_url'] ) . '" alt="" loading="lazy" decoding="async">'; | |
| 464 | + } else { | |
| 465 | + $initials = $initials ? $initials : 'P'; | |
| 466 | + $initials = function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $initials ) : strtoupper( $initials ); | |
| 467 | + $html .= esc_html( $initials ); | |
| 468 | + } | |
| 469 | + $html .= '</span>'; | |
| 300 | 470 | } |
| 301 | - $html .= '</span>'; | |
| 302 | - $html .= '<span class="wpbc_booking_appointment__choice_body"><strong>' . esc_html( $provider['title'] ) . '</strong><span class="wpbc_booking_appointment__choice_meta">' . esc_html( $provider_meta ) . '</span></span>'; | |
| 471 | + $html .= '<span class="wpbc_booking_appointment__choice_body">'; | |
| 472 | + $html .= ! empty( $config['show_resource_title'] ) | |
| 473 | + ? '<strong>' . esc_html( $provider['title'] ) . '</strong>' | |
| 474 | + : '<span class="screen-reader-text">' . esc_html( $provider['title'] ) . '</span>'; | |
| 475 | + $html .= '<span class="wpbc_booking_appointment__choice_meta">' . esc_html( $provider_meta ) . '</span></span>'; | |
| 303 | 476 | $html .= '<span class="wpbc_booking_appointment__choice_mark" aria-hidden="true"></span></label>'; |
| 304 | 477 | } |
| 305 | - $html .= '</div><input type="hidden" name="wpbc_appointment_config" value="' . esc_attr( $config_token ) . '">'; | |
| 478 | + $html .= ( $catalog_enabled ? wpbc_booking_appointment_render_catalog_close( 'providers' ) : '</div>' ); | |
| 479 | + $html .= '<input type="hidden" name="wpbc_appointment_config" value="' . esc_attr( $config_token ) . '">'; | |
| 306 | 480 | $html .= '<div class="wpbc_booking_appointment__actions">'; |
| 307 | 481 | if ( empty( $config['service_id'] ) ) { |
| 308 | 482 | $html .= '<button type="button" class="wpbc_button wpbc_button_secondary wpbc_booking_appointment__back" data-appointment-back="service">' . esc_html__( 'Back', 'booking' ) . '</button>'; |
| 309 | 483 | } |
| @@ -376,14 +550,16 @@ | ||
| 376 | 550 | * calendar initialization for markup that will subsequently be discarded. |
| 377 | 551 | * |
| 378 | 552 | * @param string $form_slug Resolved form slug. |
| 379 | 553 | * @param int $provider_id Provider resource ID. |
| 554 | + * @param string $form_status Form source status: published or preview. | |
| 380 | 555 | * |
| 381 | 556 | * @return bool True when Start Time is present or an extension allows the form. |
| 382 | 557 | */ |
| 383 | -function wpbc_booking_appointment_form_has_start_time( $form_slug, $provider_id ) { | |
| 558 | +function wpbc_booking_appointment_form_has_start_time( $form_slug, $provider_id, $form_status = 'published' ) { | |
| 384 | 559 | global $wpdb; |
| 385 | 560 | |
| 561 | + $form_status = 'preview' === sanitize_key( (string) $form_status ) ? 'preview' : 'published'; | |
| 386 | 562 | $has_start_time = false; |
| 387 | 563 | if ( class_exists( 'WPBC_FE_Form_Source_Resolver' ) ) { |
| 388 | 564 | $resolution = WPBC_FE_Form_Source_Resolver::resolve( |
| 389 | 565 | array( |
| @@ -388,13 +564,17 @@ | ||
| 388 | 564 | $resolution = WPBC_FE_Form_Source_Resolver::resolve( |
| 389 | 565 | array( |
| 390 | 566 | 'resource_id' => absint( $provider_id ), |
| 391 | 567 | 'form_slug' => sanitize_text_field( $form_slug ), |
| 392 | - 'form_status' => 'published', | |
| 568 | + 'form_status' => $form_status, | |
| 393 | 569 | ) |
| 394 | 570 | ); |
| 395 | 571 | $form_id = ! empty( $resolution['bfb_loader_args']['form_id'] ) ? absint( $resolution['bfb_loader_args']['form_id'] ) : 0; |
| 396 | - if ( $form_id ) { | |
| 572 | + if ( 'preview' === $form_status && ! empty( $resolution['bfb_loader_args'] ) && function_exists( 'wpbc_bfb_get_booking_form_pair' ) ) { | |
| 573 | + $form_pair = wpbc_bfb_get_booking_form_pair( $resolution['bfb_loader_args'] ); | |
| 574 | + $form_source = is_array( $form_pair ) && isset( $form_pair['form'] ) ? (string) $form_pair['form'] : ''; | |
| 575 | + $has_start_time = false !== stripos( $form_source, 'starttime' ); | |
| 576 | + } elseif ( $form_id ) { | |
| 397 | 577 | $form_source = $wpdb->get_var( $wpdb->prepare( "SELECT advanced_form FROM {$wpdb->prefix}booking_form_structures WHERE booking_form_id = %d AND status = %s LIMIT 1", $form_id, 'published' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 398 | 578 | $has_start_time = is_string( $form_source ) && false !== stripos( $form_source, 'starttime' ); |
| 399 | 579 | } |
| 400 | 580 | } |
| @@ -481,11 +661,27 @@ | ||
| 481 | 661 | */ |
| 482 | 662 | function wpbc_booking_appointment_render_booking_form( $service, $provider, $config, $config_token ) { |
| 483 | 663 | $provider_id = absint( $provider['provider_id'] ); |
| 484 | 664 | $form_slug = wpbc_booking_appointment_resolve_form_slug( $service, $provider_id, $config ); |
| 665 | + /** | |
| 666 | + * Filters the Form Builder source status used by an Appointment form. | |
| 667 | + * | |
| 668 | + * The normal public workflow always uses a published form. The authenticated | |
| 669 | + * Form Builder preview service temporarily changes this to `preview` only for | |
| 670 | + * an exact, user-bound transient snapshot. | |
| 671 | + * | |
| 672 | + * @param string $form_status Existing source status. Default `published`. | |
| 673 | + * @param string $form_slug Resolved Form Builder slug. | |
| 674 | + * @param int $provider_id Selected Provider resource ID. | |
| 675 | + * @param array<string,mixed> $config Signed Appointment shortcode configuration. | |
| 676 | + */ | |
| 677 | + $filtered_form_status = apply_filters( 'wpbc_booking_appointment_form_status', 'published', $form_slug, $provider_id, $config ); | |
| 678 | + $form_status = is_scalar( $filtered_form_status ) && 'preview' === sanitize_key( (string) $filtered_form_status ) | |
| 679 | + ? 'preview' | |
| 680 | + : 'published'; | |
| 485 | 681 | $allow_past = wpbc_booking_appointment_is_past_booking_enabled( $config ); |
| 486 | 682 | $require_start_time = (bool) apply_filters( 'wpbc_booking_appointment_require_start_time', true, $service, $provider, $form_slug ); |
| 487 | - if ( $require_start_time && ! wpbc_booking_appointment_form_has_start_time( $form_slug, $provider_id ) ) { | |
| 683 | + if ( $require_start_time && ! wpbc_booking_appointment_form_has_start_time( $form_slug, $provider_id, $form_status ) ) { | |
| 488 | 684 | $error_data = array(); |
| 489 | 685 | $service_settings_url = wpbc_booking_appointment_get_service_form_settings_url( $service['service_id'] ); |
| 490 | 686 | if ( $service_settings_url ) { |
| 491 | 687 | $error_data = array( |
| @@ -506,19 +702,26 @@ | ||
| 506 | 702 | return new WP_Error( 'appointment_duplicate_provider_form', __( 'This Provider already has an open Appointment form on this page. Complete or restart that Appointment before opening another form for the same Provider.', 'booking' ) ); |
| 507 | 703 | } |
| 508 | 704 | $rendered_provider_ids[ $provider_id ] = true; |
| 509 | 705 | |
| 706 | + $calendar_request_overrides = class_exists( 'WPBC_BFB_Preview_Service' ) | |
| 707 | + ? WPBC_BFB_Preview_Service::get_instance()->get_current_preview_calendar_request_overrides() | |
| 708 | + : array(); | |
| 709 | + // Appointment policy remains authoritative for past-date access while the | |
| 710 | + // signed preview supplies only its allow-listed Date Selection draft values. | |
| 711 | + $calendar_request_overrides['allow_past'] = $allow_past ? 1 : 0; | |
| 712 | + | |
| 510 | 713 | $render_params = array( |
| 511 | 714 | 'is_echo' => 0, |
| 512 | 715 | 'resource_id' => $provider_id, |
| 513 | 716 | 'cal_count' => absint( $config['cal_count'] ), |
| 514 | 717 | 'custom_booking_form' => $form_slug, |
| 718 | + 'form_status' => $form_status, | |
| 515 | 719 | 'shortcode_param__options' => $config['options'], |
| 516 | 720 | 'calendar_dates_start' => $config['calendar_dates_start'], |
| 517 | 721 | 'calendar_dates_end' => $config['calendar_dates_end'], |
| 518 | - 'calendar_request_overrides' => array( | |
| 519 | - 'allow_past' => $allow_past ? 1 : 0, | |
| 520 | - ), | |
| 722 | + 'booking_workflow' => 'appointment', | |
| 723 | + 'calendar_request_overrides' => $calendar_request_overrides, | |
| 521 | 724 | ); |
| 522 | 725 | if ( ! empty( $config['start_month_calendar'] ) && is_array( $config['start_month_calendar'] ) ) { |
| 523 | 726 | $render_params['start_month_calendar'] = array( |
| 524 | 727 | absint( $config['start_month_calendar'][0] ), |
| @@ -553,22 +756,54 @@ | ||
| 553 | 756 | ? number_format( (float) $service['base_cost'], 2, '.', '' ) |
| 554 | 757 | : ''; |
| 555 | 758 | $submission_context = wpbc_booking_appointment_encode_submission_context( $config, $service['service_id'], $provider_id ); |
| 556 | 759 | $html = wpbc_booking_appointment_render_progress( 3, $config ); |
| 557 | - $html .= '<div class="wpbc_booking_appointment__booking_header">'; | |
| 760 | + $html .= wpbc_booking_appointment_render_booking_header( $service, $provider, $config ); | |
| 761 | + $html .= '<div class="wpbc_booking_appointment__native_form" data-service-id="' . absint( $service['service_id'] ) . '" data-provider-id="' . $provider_id . '" data-service-title="' . esc_attr( $service['title'] ) . '" data-provider-title="' . esc_attr( $provider['title'] ) . '" data-duration="' . esc_attr( $duration_value ) . '" data-duration-label="' . esc_attr( wpbc_booking_appointment_format_duration( $duration_minutes ) ) . '" data-buffer-before="' . absint( $service['buffer_before_minutes'] ) . '" data-buffer-after="' . absint( $service['buffer_after_minutes'] ) . '" data-service-cost="' . esc_attr( $service_cost ) . '" data-service-cost-label="' . esc_attr( wpbc_booking_appointment_format_service_cost_text( $service_cost, $provider_id ) ) . '" data-form-slug="' . esc_attr( $form_slug ) . '" data-config-token="' . esc_attr( $config_token ) . '" data-appointment-context-token="' . esc_attr( $submission_context ) . '" data-allow-past="' . ( $allow_past ? '1' : '0' ) . '">'; | |
| 762 | + $html .= $form_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 763 | + $html .= '</div>'; | |
| 764 | + | |
| 765 | + return $html; | |
| 766 | +} | |
| 767 | + | |
| 768 | +/** | |
| 769 | + * Render the selected Appointment summary above the native Booking Form. | |
| 770 | + * | |
| 771 | + * The Service picture is decorative because the adjacent text identifies the | |
| 772 | + * selected Service. Invalid or unsupported picture URLs are omitted even when | |
| 773 | + * filtered Service data bypasses repository normalization. | |
| 774 | + * | |
| 775 | + * @param array<string,mixed> $service Effective Service presentation data. | |
| 776 | + * @param array<string,mixed> $provider Selected Provider presentation data. | |
| 777 | + * @param array<string,mixed> $config Normalized shortcode configuration. | |
| 778 | + * | |
| 779 | + * @return string Escaped Appointment header markup. | |
| 780 | + */ | |
| 781 | +function wpbc_booking_appointment_render_booking_header( $service, $provider, $config ) { | |
| 782 | + $provider_id = absint( isset( $provider['provider_id'] ) ? $provider['provider_id'] : 0 ); | |
| 783 | + $duration_minutes = absint( isset( $service['duration_minutes'] ) ? $service['duration_minutes'] : 0 ); | |
| 784 | + $service_cost = isset( $service['base_cost'] ) ? $service['base_cost'] : ''; | |
| 785 | + $service_title = isset( $service['title'] ) ? (string) $service['title'] : ''; | |
| 786 | + $provider_title = isset( $provider['title'] ) ? (string) $provider['title'] : ''; | |
| 787 | + $service_picture_url = isset( $service['picture_url'] ) ? (string) $service['picture_url'] : ''; | |
| 788 | + $safe_picture_url = esc_url( $service_picture_url ); | |
| 789 | + $show_service_image = ! isset( $config['show_resource_image'] ) || (bool) $config['show_resource_image']; | |
| 558 | 790 | $appointment_summary = wpbc_booking_appointment_join_summary_parts( |
| 559 | 791 | array( |
| 560 | 792 | wpbc_booking_appointment_format_duration( $duration_minutes ), |
| 561 | - $provider['title'], | |
| 562 | - wpbc_booking_appointment_format_service_cost_text( $service['base_cost'], $provider_id ), | |
| 793 | + $provider_title, | |
| 794 | + wpbc_booking_appointment_format_service_cost_text( $service_cost, $provider_id ), | |
| 563 | 795 | ) |
| 564 | 796 | ); |
| 565 | 797 | $summary_label = wpbc_frontend_messages__get( 'message_appointment_summary_label', array(), $provider_id ); |
| 566 | 798 | $start_over_label = wpbc_frontend_messages__get( 'message_appointment_start_over', array(), $provider_id ); |
| 567 | - $html .= '<div><span>' . esc_html( $summary_label ) . '</span><strong>' . esc_html( $service['title'] ) . '</strong><small>' . esc_html( $appointment_summary ) . '</small></div>'; | |
| 568 | - $html .= '<a class="wpbc_button wpbc_button_secondary wpbc_booking_appointment__change" data-wpbc-appointment-action="start-over" href="' . esc_url( wpbc_booking_appointment_get_fallback_url( $config ) ) . '">' . esc_html( $start_over_label ) . '</a></div>'; | |
| 569 | - $html .= '<div class="wpbc_booking_appointment__native_form" data-service-id="' . absint( $service['service_id'] ) . '" data-provider-id="' . $provider_id . '" data-service-title="' . esc_attr( $service['title'] ) . '" data-provider-title="' . esc_attr( $provider['title'] ) . '" data-duration="' . esc_attr( $duration_value ) . '" data-duration-label="' . esc_attr( wpbc_booking_appointment_format_duration( $duration_minutes ) ) . '" data-buffer-before="' . absint( $service['buffer_before_minutes'] ) . '" data-buffer-after="' . absint( $service['buffer_after_minutes'] ) . '" data-service-cost="' . esc_attr( $service_cost ) . '" data-service-cost-label="' . esc_attr( wpbc_booking_appointment_format_service_cost_text( $service_cost, $provider_id ) ) . '" data-form-slug="' . esc_attr( $form_slug ) . '" data-config-token="' . esc_attr( $config_token ) . '" data-appointment-context-token="' . esc_attr( $submission_context ) . '" data-allow-past="' . ( $allow_past ? '1' : '0' ) . '">'; | |
| 570 | - $html .= $form_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 799 | + | |
| 800 | + $html = '<div class="wpbc_booking_appointment__booking_header">'; | |
| 801 | + if ( $show_service_image && '' !== $safe_picture_url ) { | |
| 802 | + $html .= '<span class="wpbc_booking_appointment__service_image wpbc_booking_appointment__booking_header_image" aria-hidden="true"><img src="' . esc_url( $service_picture_url ) . '" alt="" loading="lazy" decoding="async"></span>'; | |
| 803 | + } | |
| 804 | + $html .= '<div class="wpbc_booking_appointment__booking_header_content"><span>' . esc_html( $summary_label ) . '</span><strong>' . esc_html( $service_title ) . '</strong><small>' . esc_html( $appointment_summary ) . '</small></div>'; | |
| 805 | + $html .= '<a class="wpbc_button wpbc_button_secondary wpbc_booking_appointment__change" data-wpbc-appointment-action="start-over" href="' . esc_url( wpbc_booking_appointment_get_fallback_url( $config ) ) . '">' . esc_html( $start_over_label ) . '</a>'; | |
| 571 | 806 | $html .= '</div>'; |
| 572 | 807 | |
| 573 | 808 | return $html; |
| 574 | 809 | } |