PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
← All changes | includes/booking-appointment/booking-appointment__render.php +274 -39 11.6 → 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 }