booking
/
includes
/
page-setup-wizard
/
step-booking-form-template
/
class-wpbc-setup-wizard-booking-form-time-options.php
class-wpbc-setup-wizard-booking-form-time-options.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-booking-form-template/class-wpbc-setup-wizard-booking-form-time-options.php
| 1 | <?php |
| 2 | /** |
| 3 | * Booking Form time-option projection for the Setup Wizard. |
| 4 | * |
| 5 | * @package Booking Calendar |
| 6 | */ |
| 7 | |
| 8 | if ( ! defined( 'ABSPATH' ) ) { |
| 9 | exit; |
| 10 | } |
| 11 | |
| 12 | /** |
| 13 | * Apply the saved Setup Wizard time proposal to one Form Builder definition. |
| 14 | * |
| 15 | * The route-specific time steps deliberately store presentation-neutral |
| 16 | * `HH:MM` values. This service is the single integration boundary that projects |
| 17 | * those values into both representations owned by Form Builder: the structured |
| 18 | * field tree used by the editor and the advanced shortcode source used by the |
| 19 | * public renderer. Keeping the transformation here makes preview and save use |
| 20 | * the same result without coupling the reusable time editor to Form Builder. |
| 21 | */ |
| 22 | final class WPBC_Setup_Wizard_Booking_Form_Time_Options { |
| 23 | |
| 24 | /** @var WPBC_Setup_Wizard_Start_End_Times */ |
| 25 | private $start_end_times; |
| 26 | |
| 27 | /** @var WPBC_Setup_Wizard_Start_Duration_Times */ |
| 28 | private $start_duration_times; |
| 29 | |
| 30 | /** @var WPBC_Setup_Wizard_Fixed_Time_Slots */ |
| 31 | private $fixed_time_slots; |
| 32 | |
| 33 | /** |
| 34 | * Build the projector around the route-specific time validators. |
| 35 | * |
| 36 | * @param WPBC_Setup_Wizard_Start_End_Times|null $start_end_times Optional Start/End validator. |
| 37 | * @param WPBC_Setup_Wizard_Start_Duration_Times|null $start_duration_times Optional Start/Duration validator. |
| 38 | * @param WPBC_Setup_Wizard_Fixed_Time_Slots|null $fixed_time_slots Optional Fixed Time Slots validator. |
| 39 | */ |
| 40 | public function __construct( $start_end_times = null, $start_duration_times = null, $fixed_time_slots = null ) { |
| 41 | $this->start_end_times = $start_end_times instanceof WPBC_Setup_Wizard_Start_End_Times |
| 42 | ? $start_end_times |
| 43 | : new WPBC_Setup_Wizard_Start_End_Times(); |
| 44 | $this->start_duration_times = $start_duration_times instanceof WPBC_Setup_Wizard_Start_Duration_Times |
| 45 | ? $start_duration_times |
| 46 | : new WPBC_Setup_Wizard_Start_Duration_Times( $this->start_end_times ); |
| 47 | $this->fixed_time_slots = $fixed_time_slots instanceof WPBC_Setup_Wizard_Fixed_Time_Slots |
| 48 | ? $fixed_time_slots |
| 49 | : new WPBC_Setup_Wizard_Fixed_Time_Slots( $this->start_end_times ); |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Build a server-owned projection context from normalized wizard values. |
| 54 | * |
| 55 | * @param array<string,mixed> $wizard_values Checkpoint values keyed by step ID. |
| 56 | * |
| 57 | * @return array{customer_journey:string,start_end_times:mixed,start_duration_times:mixed,fixed_time_slots:mixed,time_format:string} Projection context. |
| 58 | */ |
| 59 | public function get_context_from_values( array $wizard_values ) { |
| 60 | $customer_journey = isset( $wizard_values['customer_journey']['customer_journey'] ) |
| 61 | ? sanitize_key( (string) $wizard_values['customer_journey']['customer_journey'] ) |
| 62 | : ''; |
| 63 | $start_end_times = isset( $wizard_values['start_end_times']['start_end_times'] ) |
| 64 | ? $wizard_values['start_end_times']['start_end_times'] |
| 65 | : array(); |
| 66 | $start_duration_times = isset( $wizard_values['start_duration_times']['start_duration_times'] ) |
| 67 | ? $wizard_values['start_duration_times']['start_duration_times'] |
| 68 | : array(); |
| 69 | $fixed_time_slots = isset( $wizard_values['fixed_time_slots']['fixed_time_slots'] ) |
| 70 | ? $wizard_values['fixed_time_slots']['fixed_time_slots'] |
| 71 | : array(); |
| 72 | $time_format = isset( $wizard_values['date_time_formats']['time_format'] ) && is_scalar( $wizard_values['date_time_formats']['time_format'] ) |
| 73 | ? (string) $wizard_values['date_time_formats']['time_format'] |
| 74 | : ''; |
| 75 | |
| 76 | if ( ! in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ) { |
| 77 | $time_format = function_exists( 'get_bk_option' ) ? (string) get_bk_option( 'booking_time_format' ) : 'H:i'; |
| 78 | } |
| 79 | if ( ! in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ) { |
| 80 | $time_format = 'H:i'; |
| 81 | } |
| 82 | |
| 83 | return array( |
| 84 | 'customer_journey' => $customer_journey, |
| 85 | 'start_end_times' => $start_end_times, |
| 86 | 'start_duration_times' => $start_duration_times, |
| 87 | 'fixed_time_slots' => $fixed_time_slots, |
| 88 | 'time_format' => $time_format, |
| 89 | ); |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Project saved choices into a read-only preview payload. |
| 94 | * |
| 95 | * Templates without fields owned by the active journey are returned unchanged. |
| 96 | * Non-time journeys are also unchanged, which prevents unrelated customer |
| 97 | * journeys from inheriting stale time proposals from an earlier route. |
| 98 | * |
| 99 | * @param array<string,mixed> $preview_payload Server-owned preview payload. |
| 100 | * @param array<string,mixed> $projection_context Server-owned wizard context. |
| 101 | * |
| 102 | * @return array<string,mixed>|WP_Error Transformed payload or validation error. |
| 103 | */ |
| 104 | public function apply_to_preview_payload( array $preview_payload, array $projection_context ) { |
| 105 | if ( ! $this->should_project( $projection_context ) ) { |
| 106 | return $preview_payload; |
| 107 | } |
| 108 | |
| 109 | $structure = isset( $preview_payload['structure'] ) && is_array( $preview_payload['structure'] ) |
| 110 | ? $preview_payload['structure'] |
| 111 | : array(); |
| 112 | $advanced_form = isset( $preview_payload['advanced_form'] ) && is_scalar( $preview_payload['advanced_form'] ) |
| 113 | ? (string) $preview_payload['advanced_form'] |
| 114 | : ''; |
| 115 | $transformed = $this->transform_definition( $structure, $advanced_form, $projection_context ); |
| 116 | if ( is_wp_error( $transformed ) ) { |
| 117 | return $transformed; |
| 118 | } |
| 119 | |
| 120 | $preview_payload['structure'] = $transformed['structure']; |
| 121 | $preview_payload['advanced_form'] = $transformed['advanced_form']; |
| 122 | |
| 123 | return $preview_payload; |
| 124 | } |
| 125 | |
| 126 | /** |
| 127 | * Project saved choices into a canonical Form Builder save configuration. |
| 128 | * |
| 129 | * Both Form Builder representations are changed in memory before any write. |
| 130 | * A malformed structure or failed encoding therefore aborts the operation |
| 131 | * without partially changing the Standard form. |
| 132 | * |
| 133 | * @param array<string,mixed> $form_config Server-owned FormConfig. |
| 134 | * @param array<string,mixed> $projection_context Server-owned wizard context. |
| 135 | * |
| 136 | * @return array<string,mixed>|WP_Error Transformed FormConfig or validation error. |
| 137 | */ |
| 138 | public function apply_to_form_config( array $form_config, array $projection_context ) { |
| 139 | if ( ! $this->should_project( $projection_context ) ) { |
| 140 | return $form_config; |
| 141 | } |
| 142 | |
| 143 | $structure_json = isset( $form_config['structure_json'] ) && is_scalar( $form_config['structure_json'] ) |
| 144 | ? (string) $form_config['structure_json'] |
| 145 | : ''; |
| 146 | $structure = json_decode( $structure_json, true ); |
| 147 | if ( ! is_array( $structure ) || empty( $structure ) ) { |
| 148 | return new WP_Error( |
| 149 | 'wpbc_setup_wizard_booking_form_time_structure_invalid', |
| 150 | __( 'The selected booking form cannot receive the configured times because its structure is invalid.', 'booking' ) |
| 151 | ); |
| 152 | } |
| 153 | |
| 154 | $advanced_form = isset( $form_config['advanced_form'] ) && is_scalar( $form_config['advanced_form'] ) |
| 155 | ? (string) $form_config['advanced_form'] |
| 156 | : ''; |
| 157 | $transformed = $this->transform_definition( $structure, $advanced_form, $projection_context ); |
| 158 | if ( is_wp_error( $transformed ) ) { |
| 159 | return $transformed; |
| 160 | } |
| 161 | |
| 162 | $encoded_structure = wp_json_encode( $transformed['structure'] ); |
| 163 | if ( false === $encoded_structure || '' === $encoded_structure ) { |
| 164 | return new WP_Error( |
| 165 | 'wpbc_setup_wizard_booking_form_time_structure_encode_failed', |
| 166 | __( 'The configured booking times could not be prepared for the Booking Form.', 'booking' ) |
| 167 | ); |
| 168 | } |
| 169 | |
| 170 | $form_config['structure_json'] = $encoded_structure; |
| 171 | $form_config['advanced_form'] = $transformed['advanced_form']; |
| 172 | |
| 173 | return $form_config; |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * Determine whether the active journey owns an explicit time proposal. |
| 178 | * |
| 179 | * @param array<string,mixed> $projection_context Server-owned wizard context. |
| 180 | * |
| 181 | * @return bool True when time choices must be projected. |
| 182 | */ |
| 183 | private function should_project( array $projection_context ) { |
| 184 | $customer_journey = isset( $projection_context['customer_journey'] ) |
| 185 | ? sanitize_key( (string) $projection_context['customer_journey'] ) |
| 186 | : ''; |
| 187 | |
| 188 | return WPBC_Setup_Wizard_Customer_Journey_Policy::uses_time_configuration( $customer_journey ); |
| 189 | } |
| 190 | |
| 191 | /** |
| 192 | * Transform structured fields and their matching advanced shortcodes. |
| 193 | * |
| 194 | * @param array<int|string,mixed> $structure Form Builder structure. |
| 195 | * @param string $advanced_form Advanced shortcode source. |
| 196 | * @param array<string,mixed> $projection_context Server-owned wizard context. |
| 197 | * |
| 198 | * @return array{structure:array<int|string,mixed>,advanced_form:string}|WP_Error Transformed definition. |
| 199 | */ |
| 200 | private function transform_definition( array $structure, $advanced_form, array $projection_context ) { |
| 201 | $customer_journey = isset( $projection_context['customer_journey'] ) ? sanitize_key( (string) $projection_context['customer_journey'] ) : ''; |
| 202 | $time_format = isset( $projection_context['time_format'] ) && in_array( $projection_context['time_format'], array( 'g:i a', 'g:i A', 'H:i' ), true ) |
| 203 | ? (string) $projection_context['time_format'] |
| 204 | : 'H:i'; |
| 205 | |
| 206 | if ( WPBC_Setup_Wizard_Customer_Journey_Policy::uses_fixed_time_slots_configuration( $customer_journey ) ) { |
| 207 | $validated_times = $this->fixed_time_slots->validate_fixed_time_slots( |
| 208 | isset( $projection_context['fixed_time_slots'] ) ? $projection_context['fixed_time_slots'] : array(), |
| 209 | true |
| 210 | ); |
| 211 | if ( is_wp_error( $validated_times ) ) { |
| 212 | return $validated_times; |
| 213 | } |
| 214 | |
| 215 | $options_by_type = array( |
| 216 | 'rangetime' => $this->build_range_field_options( $validated_times['time_slots'], $time_format ), |
| 217 | ); |
| 218 | } elseif ( WPBC_Setup_Wizard_Customer_Journey_Policy::uses_start_duration_configuration( $customer_journey ) ) { |
| 219 | $validated_times = $this->start_duration_times->validate_start_duration_times( |
| 220 | isset( $projection_context['start_duration_times'] ) ? $projection_context['start_duration_times'] : array(), |
| 221 | true |
| 222 | ); |
| 223 | if ( is_wp_error( $validated_times ) ) { |
| 224 | return $validated_times; |
| 225 | } |
| 226 | |
| 227 | $options_by_type = array( |
| 228 | 'starttime' => $this->build_field_options( $validated_times['start_times'], $time_format ), |
| 229 | 'durationtime' => $this->build_duration_field_options( $validated_times['duration_times'] ), |
| 230 | ); |
| 231 | } else { |
| 232 | $validated_times = $this->start_end_times->validate_start_end_times( |
| 233 | isset( $projection_context['start_end_times'] ) ? $projection_context['start_end_times'] : array(), |
| 234 | true |
| 235 | ); |
| 236 | if ( is_wp_error( $validated_times ) ) { |
| 237 | return $validated_times; |
| 238 | } |
| 239 | |
| 240 | $options_by_type = array( |
| 241 | 'starttime' => $this->build_field_options( $validated_times['start_times'], $time_format ), |
| 242 | 'endtime' => $this->build_field_options( $validated_times['end_times'], $time_format ), |
| 243 | ); |
| 244 | } |
| 245 | |
| 246 | $field_names = array(); |
| 247 | $field_counts = array(); |
| 248 | foreach ( array_keys( $options_by_type ) as $field_type ) { |
| 249 | $field_names[ $field_type ] = array( $field_type ); |
| 250 | $field_counts[ $field_type ] = 0; |
| 251 | } |
| 252 | |
| 253 | $this->replace_structure_time_options( $structure, $options_by_type, $field_names, $field_counts ); |
| 254 | |
| 255 | $advanced_result = $this->replace_advanced_time_options( |
| 256 | $advanced_form, |
| 257 | $options_by_type, |
| 258 | $field_names, |
| 259 | $field_counts |
| 260 | ); |
| 261 | if ( is_wp_error( $advanced_result ) ) { |
| 262 | return $advanced_result; |
| 263 | } |
| 264 | |
| 265 | return array( |
| 266 | 'structure' => $structure, |
| 267 | 'advanced_form' => $advanced_result, |
| 268 | ); |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Build Form Builder option DTOs in the saved user order. |
| 273 | * |
| 274 | * @param string[] $time_values Validated `HH:MM` values. |
| 275 | * @param string $time_format Validated display format. |
| 276 | * |
| 277 | * @return array<int,array{label:string,value:string,selected:bool}> Field options. |
| 278 | */ |
| 279 | private function build_field_options( array $time_values, $time_format ) { |
| 280 | $field_options = array(); |
| 281 | $timezone = new DateTimeZone( 'UTC' ); |
| 282 | |
| 283 | foreach ( $time_values as $time_value ) { |
| 284 | list( $hour, $minute ) = array_map( 'intval', explode( ':', $time_value ) ); |
| 285 | $field_options[] = array( |
| 286 | 'label' => wp_date( $time_format, ( ( $hour * 60 ) + $minute ) * MINUTE_IN_SECONDS, $timezone ), |
| 287 | 'value' => $time_value, |
| 288 | 'selected' => false, |
| 289 | ); |
| 290 | } |
| 291 | |
| 292 | return $field_options; |
| 293 | } |
| 294 | |
| 295 | /** |
| 296 | * Build Form Builder duration option DTOs in the saved user order. |
| 297 | * |
| 298 | * @param string[] $duration_values Validated `HH:MM` duration values. |
| 299 | * |
| 300 | * @return array<int,array{label:string,value:string,selected:bool}> Field options. |
| 301 | */ |
| 302 | private function build_duration_field_options( array $duration_values ) { |
| 303 | $field_options = array(); |
| 304 | foreach ( $duration_values as $duration_value ) { |
| 305 | list( $hour, $minute ) = array_map( 'intval', explode( ':', $duration_value ) ); |
| 306 | $field_options[] = array( |
| 307 | 'label' => $this->start_duration_times->format_duration_label( ( $hour * 60 ) + $minute ), |
| 308 | 'value' => $duration_value, |
| 309 | 'selected' => false, |
| 310 | ); |
| 311 | } |
| 312 | |
| 313 | return $field_options; |
| 314 | } |
| 315 | |
| 316 | /** |
| 317 | * Build Form Builder range-time options in the saved slot order. |
| 318 | * |
| 319 | * @param array<int,array{start_time:string,end_time:string}> $time_slots Validated fixed-slot ranges. |
| 320 | * @param string $time_format Validated display format. |
| 321 | * |
| 322 | * @return array<int,array{label:string,value:string,selected:bool}> Field options. |
| 323 | */ |
| 324 | private function build_range_field_options( array $time_slots, $time_format ) { |
| 325 | $field_options = array(); |
| 326 | |
| 327 | foreach ( $time_slots as $time_slot ) { |
| 328 | $start_time = (string) $time_slot['start_time']; |
| 329 | $end_time = (string) $time_slot['end_time']; |
| 330 | $field_options[] = array( |
| 331 | 'label' => $this->fixed_time_slots->format_time_label( $start_time, $time_format ) . ' - ' . $this->fixed_time_slots->format_time_label( $end_time, $time_format ), |
| 332 | 'value' => $start_time . ' - ' . $end_time, |
| 333 | 'selected' => false, |
| 334 | ); |
| 335 | } |
| 336 | |
| 337 | return $field_options; |
| 338 | } |
| 339 | |
| 340 | /** |
| 341 | * Recursively replace only Form Builder fields owned by the active journey. |
| 342 | * |
| 343 | * @param array<int|string,mixed> $nodes Structure subtree, modified in place. |
| 344 | * @param array<string,array<int,array{label:string,value:string,selected:bool}>> $options_by_type Options keyed by field type. |
| 345 | * @param array<string,string[]> $field_names Discovered field names, modified in place. |
| 346 | * @param array<string,int> $field_counts Discovered field counts, modified in place. |
| 347 | * |
| 348 | * @return void |
| 349 | */ |
| 350 | private function replace_structure_time_options( array &$nodes, array $options_by_type, array &$field_names, array &$field_counts ) { |
| 351 | if ( isset( $nodes['type'], $nodes['data'] ) && 'field' === $nodes['type'] && is_array( $nodes['data'] ) ) { |
| 352 | $field_type = isset( $nodes['data']['type'] ) ? sanitize_key( (string) $nodes['data']['type'] ) : ''; |
| 353 | if ( ! isset( $options_by_type[ $field_type ] ) && isset( $nodes['data']['usage_key'] ) ) { |
| 354 | $field_type = sanitize_key( (string) $nodes['data']['usage_key'] ); |
| 355 | } |
| 356 | |
| 357 | if ( isset( $options_by_type[ $field_type ] ) ) { |
| 358 | $nodes['data']['options'] = $options_by_type[ $field_type ]; |
| 359 | if ( array_key_exists( 'default_value', $nodes['data'] ) ) { |
| 360 | $nodes['data']['default_value'] = ''; |
| 361 | } |
| 362 | if ( array_key_exists( 'defaultValue', $nodes['data'] ) ) { |
| 363 | $nodes['data']['defaultValue'] = ''; |
| 364 | } |
| 365 | ++$field_counts[ $field_type ]; |
| 366 | if ( isset( $nodes['data']['name'] ) && is_scalar( $nodes['data']['name'] ) ) { |
| 367 | $field_name = sanitize_key( (string) $nodes['data']['name'] ); |
| 368 | if ( '' !== $field_name ) { |
| 369 | $field_names[ $field_type ][] = $field_name; |
| 370 | } |
| 371 | } |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | foreach ( $nodes as &$node ) { |
| 376 | if ( is_array( $node ) ) { |
| 377 | $this->replace_structure_time_options( $node, $options_by_type, $field_names, $field_counts ); |
| 378 | } |
| 379 | } |
| 380 | unset( $node ); |
| 381 | } |
| 382 | |
| 383 | /** |
| 384 | * Replace matching selectbox shortcodes while preserving field modifiers. |
| 385 | * |
| 386 | * @param string $advanced_form Advanced shortcode source. |
| 387 | * @param array<string,array<int,array{label:string,value:string,selected:bool}>> $options_by_type Options keyed by field type. |
| 388 | * @param array<string,string[]> $field_names Canonical and discovered field names. |
| 389 | * @param array<string,int> $field_counts Structured fields found by type. |
| 390 | * |
| 391 | * @return string|WP_Error Transformed source or a synchronization error. |
| 392 | */ |
| 393 | private function replace_advanced_time_options( $advanced_form, array $options_by_type, array $field_names, array $field_counts ) { |
| 394 | $advanced_form = (string) $advanced_form; |
| 395 | |
| 396 | foreach ( $options_by_type as $field_type => $field_options ) { |
| 397 | $names = array_values( array_unique( array_filter( $field_names[ $field_type ] ) ) ); |
| 398 | $escaped_names = array_map( 'preg_quote', $names, array_fill( 0, count( $names ), '/' ) ); |
| 399 | $pattern = '/\[(selectbox\*?)\s+(' . implode( '|', $escaped_names ) . ')(?=\s|\])([^\]]*)\]/i'; |
| 400 | $replacement_count = 0; |
| 401 | $advanced_form = preg_replace_callback( |
| 402 | $pattern, |
| 403 | function ( $matches ) use ( $field_options ) { |
| 404 | $tokens = array(); |
| 405 | if ( preg_match_all( '/"((?:\\\\.|[^"\\\\])*)"|\'((?:\\\\.|[^\'\\\\])*)\'/', $matches[3], $quoted_tokens, PREG_SET_ORDER ) ) { |
| 406 | foreach ( $quoted_tokens as $quoted_token ) { |
| 407 | $double_quoted_value = isset( $quoted_token[1] ) ? (string) $quoted_token[1] : ''; |
| 408 | $single_quoted_value = isset( $quoted_token[2] ) ? (string) $quoted_token[2] : ''; |
| 409 | $token_value = '' !== $double_quoted_value ? $double_quoted_value : $single_quoted_value; |
| 410 | if ( 2 <= strlen( $token_value ) && '@@' === substr( $token_value, -2 ) ) { |
| 411 | $tokens[] = $quoted_token[0]; |
| 412 | break; |
| 413 | } |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | $modifiers = preg_replace( '/\bdefault(?::|=)(?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|[^\s\]]+)/i', '', $matches[3] ); |
| 418 | $modifiers = preg_replace( '/(?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\')/', '', (string) $modifiers ); |
| 419 | $modifiers = trim( preg_replace( '/\s+/', ' ', (string) $modifiers ) ); |
| 420 | |
| 421 | foreach ( $field_options as $field_option ) { |
| 422 | $option_token = (string) $field_option['label'] . '@@' . (string) $field_option['value']; |
| 423 | $tokens[] = '"' . str_replace( array( '\\', '"' ), array( '\\\\', '\\"' ), $option_token ) . '"'; |
| 424 | } |
| 425 | |
| 426 | return '[' . $matches[1] . ' ' . $matches[2] |
| 427 | . ( '' !== $modifiers ? ' ' . $modifiers : '' ) |
| 428 | . ' ' . implode( ' ', $tokens ) . ']'; |
| 429 | }, |
| 430 | $advanced_form, |
| 431 | -1, |
| 432 | $replacement_count |
| 433 | ); |
| 434 | |
| 435 | if ( null === $advanced_form ) { |
| 436 | return new WP_Error( |
| 437 | 'wpbc_setup_wizard_booking_form_time_shortcode_invalid', |
| 438 | __( 'The configured booking times could not be applied to the Booking Form source.', 'booking' ) |
| 439 | ); |
| 440 | } |
| 441 | if ( $field_counts[ $field_type ] > 0 && 0 === $replacement_count && '' !== trim( $advanced_form ) ) { |
| 442 | return new WP_Error( |
| 443 | 'wpbc_setup_wizard_booking_form_time_shortcode_missing', |
| 444 | __( 'The selected Booking Form contains a time field that could not be synchronized safely.', 'booking' ) |
| 445 | ); |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | return $advanced_form; |
| 450 | } |
| 451 | } |
| 452 |