booking
/
includes
/
page-setup-wizard
/
step-booking-form-template
/
class-wpbc-setup-wizard-booking-form-template-module.php
class-wpbc-setup-wizard-booking-form-template-module.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-booking-form-template/class-wpbc-setup-wizard-booking-form-template-module.php
| 1 | <?php |
| 2 | /** |
| 3 | * Reusable module adapter for the Booking Form Template setup editor. |
| 4 | * |
| 5 | * @package Booking Calendar |
| 6 | */ |
| 7 | |
| 8 | if ( ! defined( 'ABSPATH' ) ) { |
| 9 | exit; |
| 10 | } |
| 11 | |
| 12 | /** |
| 13 | * Expose the template selector through the common setup-module contract. |
| 14 | */ |
| 15 | final class WPBC_Setup_Wizard_Booking_Form_Template_Module implements WPBC_Setup_Wizard_Step_Module, WPBC_Setup_Wizard_Contextual_Values_Validator { |
| 16 | |
| 17 | /** @var WPBC_Setup_Wizard_Booking_Form_Templates */ |
| 18 | private $templates; |
| 19 | |
| 20 | /** @var WPBC_Setup_Wizard_Booking_Form_Preview */ |
| 21 | private $preview; |
| 22 | |
| 23 | /** @var WPBC_Setup_Wizard_Booking_Form_Time_Options */ |
| 24 | private $time_options; |
| 25 | |
| 26 | /** |
| 27 | * Build the module around the read-only Form Builder registry adapter. |
| 28 | * |
| 29 | * @param WPBC_Setup_Wizard_Booking_Form_Templates|null $templates Optional registry adapter for testing or composition. |
| 30 | * @param WPBC_Setup_Wizard_Booking_Form_Preview|null $preview Optional preview coordinator for testing or composition. |
| 31 | * @param WPBC_Setup_Wizard_Booking_Form_Time_Options|null $time_options Optional time projector for testing or composition. |
| 32 | */ |
| 33 | public function __construct( $templates = null, $preview = null, $time_options = null ) { |
| 34 | $this->templates = $templates instanceof WPBC_Setup_Wizard_Booking_Form_Templates |
| 35 | ? $templates |
| 36 | : new WPBC_Setup_Wizard_Booking_Form_Templates(); |
| 37 | $this->time_options = $time_options instanceof WPBC_Setup_Wizard_Booking_Form_Time_Options |
| 38 | ? $time_options |
| 39 | : new WPBC_Setup_Wizard_Booking_Form_Time_Options(); |
| 40 | $this->preview = $preview instanceof WPBC_Setup_Wizard_Booking_Form_Preview |
| 41 | ? $preview |
| 42 | : new WPBC_Setup_Wizard_Booking_Form_Preview( $this->templates, $this->time_options ); |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * Return the stable module and step identifier. |
| 47 | * |
| 48 | * @return string Stable identifier. |
| 49 | */ |
| 50 | public function get_step_id() { |
| 51 | return 'booking_form_template'; |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Return the rail and template metadata contributed by this module. |
| 56 | * |
| 57 | * @return array{id:string,label:string,template:string,footer_note:string} Step definition. |
| 58 | */ |
| 59 | public function get_step_definition() { |
| 60 | return array( |
| 61 | 'id' => $this->get_step_id(), |
| 62 | 'label' => __( 'Booking form', 'booking' ), |
| 63 | 'template' => 'step-booking-form-template', |
| 64 | 'footer_note' => __( 'Create or update the selected Booking Form when you continue. Guided Appointment also assigns it to the saved Services.', 'booking' ), |
| 65 | ); |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Return the absolute server-owned template path. |
| 70 | * |
| 71 | * @return string Absolute template path. |
| 72 | */ |
| 73 | public function get_template_path() { |
| 74 | return __DIR__ . '/template.php'; |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Return the fields accepted by the selector. |
| 79 | * |
| 80 | * @return string[] Field allow-list. |
| 81 | */ |
| 82 | public function get_field_names() { |
| 83 | return array( 'booking_form_template', 'booking_form_usage' ); |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Return the required fields when advancing. |
| 88 | * |
| 89 | * @return string[] Required field identifiers. |
| 90 | */ |
| 91 | public function get_required_fields() { |
| 92 | return array( 'booking_form_template', 'booking_form_usage' ); |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Return the preferred Form Builder template as a read-only suggestion. |
| 97 | * |
| 98 | * @return array<string,string> Initial field value. |
| 99 | */ |
| 100 | public function get_initial_values() { |
| 101 | return array( |
| 102 | 'booking_form_template' => $this->templates->get_initial_template_slug(), |
| 103 | 'booking_form_usage' => $this->templates->get_initial_booking_form_usage(), |
| 104 | ); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Return dependencies used for recommendations and time projection. |
| 109 | * |
| 110 | * @return string[] Dependency step identifiers. |
| 111 | */ |
| 112 | public function get_context_dependencies() { |
| 113 | return array( 'customer_journey', 'start_end_times', 'start_duration_times', 'fixed_time_slots', 'date_time_formats' ); |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * Build the template selector context and its initial Direct preview. |
| 118 | * |
| 119 | * Appointment Flow intentionally remains client-loaded because its later |
| 120 | * Service and Provider requests require the signed preview URL returned by |
| 121 | * the authenticated preview endpoint. |
| 122 | * |
| 123 | * @param array<string,mixed> $field_values Validated module field values. |
| 124 | * @param array<string,mixed> $consumer_context Read-only dependency values. |
| 125 | * |
| 126 | * @return array<string,mixed> Authorized presentation context. |
| 127 | */ |
| 128 | public function get_context( array $field_values, array $consumer_context = array() ) { |
| 129 | $customer_journey = isset( $consumer_context['customer_journey']['customer_journey'] ) |
| 130 | && is_scalar( $consumer_context['customer_journey']['customer_journey'] ) |
| 131 | ? (string) $consumer_context['customer_journey']['customer_journey'] |
| 132 | : ''; |
| 133 | $has_saved_values = ! empty( $consumer_context['_step_state']['has_saved_values'] ); |
| 134 | $is_journey_transition = WPBC_Setup_Wizard_Customer_Journey_Policy::has_pending_transition_for_step( |
| 135 | isset( $consumer_context['_step_results'] ) && is_array( $consumer_context['_step_results'] ) |
| 136 | ? $consumer_context['_step_results'] |
| 137 | : array(), |
| 138 | $this->get_step_id() |
| 139 | ); |
| 140 | |
| 141 | // A newly saved journey owns the next Booking Form recommendation. Preserve |
| 142 | // an explicitly saved template during ordinary revisits, but do not carry a |
| 143 | // stale template across a deliberate restart from Customer Journey. |
| 144 | if ( ! $has_saved_values || $is_journey_transition ) { |
| 145 | $field_values['booking_form_template'] = $this->templates->get_recommended_template_slug( $customer_journey ); |
| 146 | } |
| 147 | $template_context = $this->templates->get_context( $field_values, $customer_journey ); |
| 148 | $selected_slug = isset( $template_context['values']['booking_form_template'] ) |
| 149 | ? (string) $template_context['values']['booking_form_template'] |
| 150 | : ''; |
| 151 | $selected_usage = isset( $template_context['values']['booking_form_usage'] ) |
| 152 | ? (string) $template_context['values']['booking_form_usage'] |
| 153 | : ''; |
| 154 | $booking_form_context = $this->time_options->get_context_from_values( $consumer_context ); |
| 155 | $preview_result = array(); |
| 156 | $preview_error = ''; |
| 157 | |
| 158 | if ( '' !== $selected_slug && 'direct_booking_form' === $selected_usage ) { |
| 159 | $preview_result = $this->preview->build_preview( |
| 160 | $selected_slug, |
| 161 | $selected_usage, |
| 162 | WPBC_Setup_Wizard_Booking_Form_Preview::TARGET_EMBEDDED, |
| 163 | array(), |
| 164 | $booking_form_context |
| 165 | ); |
| 166 | |
| 167 | if ( is_wp_error( $preview_result ) ) { |
| 168 | $preview_error = $preview_result->get_error_message(); |
| 169 | $preview_result = array(); |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | $step_dialogs = array( |
| 174 | array( |
| 175 | 'dialog_id' => 'wpbc-setup-wizard-booking-form-guidance-dialog', |
| 176 | 'action' => 'booking-form-template-guidance', |
| 177 | 'title' => __( 'Choose a starting template', 'booking' ), |
| 178 | 'description' => __( 'This step selects a ready-made starting template for your booking form. It does not limit how you can customize the form.', 'booking' ), |
| 179 | 'details' => array( |
| 180 | __( 'Review the interactive preview, then choose the template that best matches your booking flow.', 'booking' ), |
| 181 | __( 'After setup, open WP Booking Calendar > Settings > Forms Builder to add, remove, reorder, and configure your booking form fields.', 'booking' ), |
| 182 | ), |
| 183 | 'note' => __( 'The selected template is applied only after you choose Update booking form & continue.', 'booking' ), |
| 184 | 'cancel_label' => __( 'Close', 'booking' ), |
| 185 | 'confirm_label' => __( 'Got it', 'booking' ), |
| 186 | ), |
| 187 | ); |
| 188 | |
| 189 | if ( 'guided_appointment_flow' === $customer_journey ) { |
| 190 | $step_dialogs[] = array( |
| 191 | 'dialog_id' => 'wpbc-setup-wizard-appointment-flow-dialog', |
| 192 | 'action' => 'booking-form-appointment-flow', |
| 193 | 'title' => __( 'Use Appointment flow?', 'booking' ), |
| 194 | 'description' => __( 'Before switching, review how Appointment flow uses the selected booking form:', 'booking' ), |
| 195 | 'details' => array( |
| 196 | __( 'The selected booking form must contain a Start Time field.', 'booking' ), |
| 197 | __( 'Appointment flow uses the selected Service duration. Duration, End Time, and Time Range fields in the template are not shown, so customers cannot choose conflicting times.', 'booking' ), |
| 198 | __( 'This preview lists only Services already saved and active on your site, together with their assigned Providers. Services prepared earlier in this setup draft are not shown until setup is applied.', 'booking' ), |
| 199 | ), |
| 200 | 'note' => __( 'This remains a private preview. Switching the entry point does not publish Services, import the template, or replace an existing booking form.', 'booking' ), |
| 201 | 'cancel_label' => __( 'Keep direct booking form', 'booking' ), |
| 202 | 'confirm_label' => __( 'Use Appointment flow', 'booking' ), |
| 203 | ); |
| 204 | } |
| 205 | |
| 206 | $template_context['preview_kind'] = isset( $preview_result['preview_kind'] ) ? (string) $preview_result['preview_kind'] : ''; |
| 207 | $template_context['preview_html'] = isset( $preview_result['html'] ) ? (string) $preview_result['html'] : ''; |
| 208 | $template_context['preview_bootstrap'] = isset( $preview_result['bootstrap'] ) && is_array( $preview_result['bootstrap'] ) ? $preview_result['bootstrap'] : array(); |
| 209 | $template_context['preview_error'] = $preview_error; |
| 210 | $template_context['dialogs'] = $step_dialogs; |
| 211 | |
| 212 | return $template_context; |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Validate the selected stable Form Builder template slug. |
| 217 | * |
| 218 | * @param string $field_id Stable field identifier. |
| 219 | * @param mixed $raw_value Untrusted template slug. |
| 220 | * @param bool $is_required Whether an empty choice is invalid. |
| 221 | * |
| 222 | * @return string|WP_Error Normalized slug or an error. |
| 223 | */ |
| 224 | public function validate_field( $field_id, $raw_value, $is_required ) { |
| 225 | if ( 'booking_form_template' === $field_id ) { |
| 226 | return $this->templates->validate_template_slug( $raw_value, $is_required ); |
| 227 | } |
| 228 | |
| 229 | if ( 'booking_form_usage' === $field_id ) { |
| 230 | return $this->templates->validate_booking_form_usage( $raw_value, $is_required ); |
| 231 | } |
| 232 | |
| 233 | return new WP_Error( 'wpbc_setup_wizard_booking_form_template_field_unknown', __( 'The Booking Form editor received an unsupported field.', 'booking' ) ); |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Revalidate the selected entry point against the active Customer Journey. |
| 238 | * |
| 239 | * Field validation owns the global usage allow-list. This contextual pass is |
| 240 | * the authoritative route boundary that prevents Appointment Flow from being |
| 241 | * submitted for time-based journeys even when a stale browser tab still |
| 242 | * contains the old radio value. |
| 243 | * |
| 244 | * @param array<string,mixed> $validated_values Validated module values. |
| 245 | * @param array<string,mixed> $consumer_context Read-only dependency values. |
| 246 | * |
| 247 | * @return array<string,mixed>|WP_Error Validated values or a field-addressable error. |
| 248 | */ |
| 249 | public function validate_values_with_context( array $validated_values, array $consumer_context ) { |
| 250 | $customer_journey = isset( $consumer_context['customer_journey']['customer_journey'] ) |
| 251 | ? sanitize_key( (string) $consumer_context['customer_journey']['customer_journey'] ) |
| 252 | : ''; |
| 253 | $booking_form_usage = isset( $validated_values['booking_form_usage'] ) |
| 254 | ? $this->templates->validate_booking_form_usage_for_journey( $validated_values['booking_form_usage'], $customer_journey, true ) |
| 255 | : new WP_Error( 'wpbc_setup_wizard_booking_form_usage_required', __( 'Choose how customers will open the booking form.', 'booking' ) ); |
| 256 | |
| 257 | if ( is_wp_error( $booking_form_usage ) ) { |
| 258 | $message = $booking_form_usage->get_error_message(); |
| 259 | |
| 260 | return new WP_Error( |
| 261 | $booking_form_usage->get_error_code(), |
| 262 | $message, |
| 263 | array( 'field_errors' => array( 'booking_form_usage' => $message ) ) |
| 264 | ); |
| 265 | } |
| 266 | |
| 267 | $validated_values['booking_form_usage'] = $booking_form_usage; |
| 268 | |
| 269 | return $validated_values; |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * Enqueue compiled selector assets and translations. |
| 274 | * |
| 275 | * @param string $module_url Absolute URL to the Setup Wizard module root. |
| 276 | * @param string|false $asset_version Plugin version used for cache busting. |
| 277 | * @param string $shared_style_handle Consumer shell style handle. |
| 278 | * @param string $shared_script_handle Consumer shell script handle. |
| 279 | * |
| 280 | * @return void |
| 281 | */ |
| 282 | public function enqueue_assets( $module_url, $asset_version, $shared_style_handle, $shared_script_handle ) { |
| 283 | $module_url = trailingslashit( $module_url ); |
| 284 | $script_dependencies = array( $shared_script_handle ); |
| 285 | |
| 286 | if ( class_exists( 'WPBC_BFB_Preview_Service' ) ) { |
| 287 | WPBC_BFB_Preview_Service::get_instance()->enqueue_inline_preview_assets(); |
| 288 | $script_dependencies[] = 'wpbc-bfb-inline-preview'; |
| 289 | } |
| 290 | |
| 291 | wp_enqueue_style( 'wpbc-setup-wizard-booking-form-template', $module_url . 'step-booking-form-template/_out/step-booking-form-template.css', array( $shared_style_handle ), $asset_version ); |
| 292 | wp_enqueue_script( 'wpbc-setup-wizard-booking-form-template', $module_url . 'step-booking-form-template/_out/step-booking-form-template.js', $script_dependencies, $asset_version, true ); |
| 293 | wp_localize_script( |
| 294 | 'wpbc-setup-wizard-booking-form-template', |
| 295 | 'wpbc_setup_wizard_booking_form_template', |
| 296 | array( |
| 297 | 'ajax_url' => admin_url( 'admin-ajax.php' ), |
| 298 | 'nonce' => wp_create_nonce( WPBC_Setup_Wizard_Ajax::NONCE_ACTION ), |
| 299 | 'preview_action' => WPBC_Setup_Wizard_Booking_Form_Preview_Ajax::ACTION, |
| 300 | 'i18n' => array( |
| 301 | 'template_required' => __( 'Choose a booking form template.', 'booking' ), |
| 302 | 'usage_required' => __( 'Choose how customers will open the booking form.', 'booking' ), |
| 303 | 'no_matching_templates' => __( 'No templates match this search and filter.', 'booking' ), |
| 304 | 'preview_loading' => __( 'Loading the interactive booking form preview.', 'booking' ), |
| 305 | 'preview_error' => __( 'The interactive preview could not be loaded. Try refreshing it.', 'booking' ), |
| 306 | 'preview_open_error' => __( 'The full-site preview could not be opened. Try again.', 'booking' ), |
| 307 | ), |
| 308 | ) |
| 309 | ); |
| 310 | } |
| 311 | } |
| 312 |