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
booking / includes / page-setup-wizard / step-date-selection / class-wpbc-setup-wizard-date-selection-module.php

class-wpbc-setup-wizard-date-selection-module.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-date-selection/class-wpbc-setup-wizard-date-selection-module.php

450 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Reusable module adapter for the Setup Wizard Date Selection editor.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Expose canonical date-selection behavior through the setup-module contract.
14 */
15 final class WPBC_Setup_Wizard_Date_Selection_Module implements WPBC_Setup_Wizard_Step_Module, WPBC_Setup_Wizard_Current_Values_Module, WPBC_Setup_Wizard_Step_Values_Validator, WPBC_Setup_Wizard_Contextual_Values_Validator {
16
17 /** @var WPBC_Setup_Wizard_Date_Selection */
18 private $date_selection;
19
20 /**
21 * Build the module around the date-selection domain service.
22 *
23 * @param WPBC_Setup_Wizard_Date_Selection|null $date_selection Optional service for testing.
24 */
25 public function __construct( $date_selection = null ) {
26 $this->date_selection = $date_selection instanceof WPBC_Setup_Wizard_Date_Selection
27 ? $date_selection
28 : new WPBC_Setup_Wizard_Date_Selection();
29 }
30
31 /**
32 * Return the stable step identifier.
33 *
34 * @return string Stable identifier.
35 */
36 public function get_step_id() {
37 return 'date_selection';
38 }
39
40 /**
41 * Return rail and template metadata.
42 *
43 * @return array<string,string> Step definition.
44 */
45 public function get_step_definition() {
46 return array(
47 'id' => $this->get_step_id(),
48 'label' => __( 'Date selection', 'booking' ),
49 'template' => 'step-date-selection',
50 'footer_note' => __( 'Save & continue applies these Date Selection settings immediately.', 'booking' ),
51 );
52 }
53
54 /**
55 * Return the domain-owned allow-listed template path.
56 *
57 * @return string Absolute template path.
58 */
59 public function get_template_path() {
60 return __DIR__ . '/template.php';
61 }
62
63 /**
64 * Return the ordered Date Selection draft contract.
65 *
66 * @return string[] Field allow-list.
67 */
68 public function get_field_names() {
69 $field_names = array(
70 'date_selection_mode',
71 'date_selection_fixed_days',
72 'date_selection_fixed_weekdays',
73 'date_selection_dynamic_min',
74 'date_selection_dynamic_max',
75 'date_selection_dynamic_weekdays',
76 'date_selection_dynamic_specific',
77 'date_selection_changeover_enabled',
78 'date_selection_check_in_time',
79 'date_selection_check_out_time',
80 'date_selection_triangles',
81 'date_selection_checkout_available',
82 'date_selection_recurrent_time',
83 'date_selection_legend_enabled',
84 'date_selection_legend_show_numbers',
85 'date_selection_legend_vertical',
86 );
87
88 foreach ( $this->date_selection->get_legend_items() as $legend_item ) {
89 $field_names[] = 'date_selection_legend_item_' . $legend_item['id'];
90 $field_names[] = 'date_selection_legend_text_' . $legend_item['id'];
91 }
92
93 return $field_names;
94 }
95
96 /**
97 * Require the complete policy so later apply logic never guesses values.
98 *
99 * @return string[] Required fields.
100 */
101 public function get_required_fields() {
102 return $this->get_field_names();
103 }
104
105 /**
106 * Return current canonical settings as safe draft suggestions.
107 *
108 * @return array<string,string> Initial values.
109 */
110 public function get_initial_values() {
111 return $this->date_selection->get_initial_values();
112 }
113
114 /**
115 * Refresh the complete Date Selection policy from canonical options.
116 *
117 * @return string[] Current-value field identifiers.
118 */
119 public function get_current_value_field_names() {
120 return $this->get_field_names();
121 }
122
123 /**
124 * Return the Customer Journey dependency that owns compatible modes.
125 *
126 * @return string[] Dependency step identifiers.
127 */
128 public function get_context_dependencies() {
129 return array( 'customer_journey' );
130 }
131
132 /**
133 * Build the data-only Date Selection context and real calendar preview.
134 *
135 * @param array<string,mixed> $field_values Validated module values.
136 * @param array<string,mixed> $consumer_context Read-only dependency values.
137 *
138 * @return array<string,mixed> Authorized template context.
139 */
140 public function get_context( array $field_values, array $consumer_context = array() ) {
141 $customer_journey_id = $this->get_customer_journey_id( $consumer_context );
142 if ( '' === $customer_journey_id ) {
143 $customer_journey_id = 'guided_appointment_flow';
144 }
145 $has_saved_values = ! empty( $consumer_context['_step_state']['has_saved_values'] );
146 $is_journey_transition = $this->has_pending_customer_journey_transition(
147 isset( $consumer_context['_step_results'] ) && is_array( $consumer_context['_step_results'] )
148 ? $consumer_context['_step_results']
149 : array()
150 );
151 $journey_policy = $this->date_selection->get_customer_journey_policy( $customer_journey_id );
152 $field_values = $this->date_selection->apply_customer_journey_defaults( $field_values, $customer_journey_id, $has_saved_values, $is_journey_transition );
153
154 $resource_id = $this->get_preview_resource_id();
155 $preview_result = $this->render_calendar_preview( $resource_id, $field_values, $customer_journey_id );
156 $time_options = $this->date_selection->get_time_options( $field_values['date_selection_check_in_time'] );
157 $time_options = array_replace(
158 $time_options,
159 $this->date_selection->get_time_options( $field_values['date_selection_check_out_time'] )
160 );
161 ksort( $time_options );
162
163 return array(
164 'values' => $field_values,
165 'customer_journey_policy' => $journey_policy,
166 'weekdays' => $this->date_selection->get_weekdays(),
167 'specific_day_presets' => $this->date_selection->get_specific_day_presets(),
168 'legend_items' => $this->date_selection->get_legend_items(),
169 'legend_preview_items' => $this->render_legend_preview_items( $field_values, $resource_id ),
170 'time_options' => $time_options,
171 'supports_advanced_range_rules' => $this->date_selection->supports_advanced_range_rules(),
172 'preview_resource_id' => $resource_id,
173 'preview_html' => $preview_result['html'],
174 'preview_error' => $preview_result['error'],
175 );
176 }
177
178 /**
179 * Determine whether Customer Journey was saved after Date Selection.
180 *
181 * Every successful progressive save records the checkpoint revision observed
182 * by that operation. A newer Customer Journey result means the current Date
183 * Selection page was reached through that forward transition. Comparing
184 * existing server-owned metadata keeps the recommendation stable on refresh
185 * without adding a browser-controlled query flag or changing live settings.
186 *
187 * @param array<string,mixed> $step_results Normalized progressive-save results.
188 *
189 * @return bool True when the Customer Journey transition is pending.
190 */
191 public function has_pending_customer_journey_transition( array $step_results ) {
192 return WPBC_Setup_Wizard_Customer_Journey_Policy::has_pending_transition_for_step( $step_results, 'date_selection' );
193 }
194
195 /**
196 * Validate one module field through the domain service.
197 *
198 * @param string $field_id Stable field identifier.
199 * @param mixed $raw_value Untrusted submitted or stored value.
200 * @param bool $is_required Whether an empty value is invalid.
201 *
202 * @return string|WP_Error Normalized value or an error.
203 */
204 public function validate_field( $field_id, $raw_value, $is_required ) {
205 return $this->date_selection->validate_field( $field_id, $raw_value, $is_required );
206 }
207
208 /**
209 * Validate relationships between normalized Date Selection fields.
210 *
211 * @param array<string,mixed> $validated_values Individually validated field values.
212 *
213 * @return array<string,mixed>|WP_Error Validated values or a field-addressable error.
214 */
215 public function validate_values( array $validated_values ) {
216 if (
217 isset( $validated_values['date_selection_dynamic_min'], $validated_values['date_selection_dynamic_max'] )
218 && (int) $validated_values['date_selection_dynamic_max'] < (int) $validated_values['date_selection_dynamic_min']
219 ) {
220 $message = __( 'The maximum flexible range must be greater than or equal to the minimum range.', 'booking' );
221
222 return new WP_Error(
223 'wpbc_setup_wizard_date_selection_range_invalid',
224 $message,
225 array(
226 'field_errors' => array(
227 'date_selection_dynamic_max' => $message,
228 ),
229 )
230 );
231 }
232
233 if ( ! empty( $validated_values['date_selection_dynamic_specific'] ) ) {
234 $specific_days = array_map( 'intval', explode( ',', $validated_values['date_selection_dynamic_specific'] ) );
235 $minimum_days = isset( $validated_values['date_selection_dynamic_min'] ) ? (int) $validated_values['date_selection_dynamic_min'] : 1;
236 $maximum_days = isset( $validated_values['date_selection_dynamic_max'] ) ? (int) $validated_values['date_selection_dynamic_max'] : WPBC_Setup_Wizard_Date_Selection::MAX_DYNAMIC_DAYS;
237
238 foreach ( $specific_days as $specific_day ) {
239 if ( $specific_day < $minimum_days || $specific_day > $maximum_days ) {
240 $message = __( 'Every specific day length must be within the minimum and maximum flexible range.', 'booking' );
241
242 return new WP_Error(
243 'wpbc_setup_wizard_date_selection_specific_range_invalid',
244 $message,
245 array(
246 'field_errors' => array(
247 'date_selection_dynamic_specific' => $message,
248 ),
249 )
250 );
251 }
252 }
253 }
254
255 // A check-out date exists only for range selections. Normalize stale or
256 // tampered values before the request-local preview consumes the draft.
257 if (
258 isset( $validated_values['date_selection_mode'] )
259 && in_array( $validated_values['date_selection_mode'], array( 'single', 'multiple' ), true )
260 ) {
261 $validated_values['date_selection_checkout_available'] = 'Off';
262 }
263
264 // Match the canonical Calendar settings: changeover processing conflicts
265 // with recurrent time and an independently available check-out date.
266 if ( isset( $validated_values['date_selection_changeover_enabled'] ) && 'On' === $validated_values['date_selection_changeover_enabled'] ) {
267 $validated_values['date_selection_checkout_available'] = 'Off';
268 $validated_values['date_selection_recurrent_time'] = 'Off';
269 }
270
271 return $validated_values;
272 }
273
274 /**
275 * Enforce the selected Customer Journey after normal field validation.
276 *
277 * @param array<string,mixed> $validated_values Date Selection values after module validation.
278 * @param array<string,mixed> $consumer_context Read-only Customer Journey values.
279 *
280 * @return array<string,mixed>|WP_Error Journey-compatible values or a field-addressable error.
281 */
282 public function validate_values_with_context( array $validated_values, array $consumer_context ) {
283 $customer_journey_id = $this->get_customer_journey_id( $consumer_context );
284 if ( '' === $customer_journey_id ) {
285 return $validated_values;
286 }
287
288 return $this->date_selection->validate_values_for_customer_journey(
289 $validated_values,
290 $customer_journey_id
291 );
292 }
293
294 /**
295 * Enqueue compiled Date Selection assets and the real calendar runtime.
296 *
297 * @param string $module_url Setup Wizard module URL.
298 * @param string|false $asset_version Plugin asset version.
299 * @param string $shared_style_handle Shared wizard style handle.
300 * @param string $shared_script_handle Shared wizard script handle.
301 *
302 * @return void
303 */
304 public function enqueue_assets( $module_url, $asset_version, $shared_style_handle, $shared_script_handle ) {
305 $module_url = trailingslashit( $module_url );
306
307 wp_enqueue_style( 'wpbc-calendar' );
308 wp_enqueue_script( 'wpbc_all' );
309 wp_enqueue_style( 'wpbc-setup-wizard-date-selection', $module_url . 'step-date-selection/_out/step-date-selection.css', array( $shared_style_handle, 'wpbc-calendar' ), $asset_version );
310 wp_enqueue_script( 'wpbc-setup-wizard-date-selection', $module_url . 'step-date-selection/_out/step-date-selection.js', array( $shared_script_handle, 'wpbc_all' ), $asset_version, true );
311 wp_localize_script(
312 'wpbc-setup-wizard-date-selection',
313 'wpbc_setup_wizard_date_selection',
314 array(
315 'max_fixed_days' => WPBC_Setup_Wizard_Date_Selection::MAX_FIXED_DAYS,
316 'max_dynamic_days' => WPBC_Setup_Wizard_Date_Selection::MAX_DYNAMIC_DAYS,
317 'preview_nonce' => wp_create_nonce( WPBC_Setup_Wizard_Date_Selection::PREVIEW_NONCE_ACTION ),
318 'i18n' => array(
319 'mode_required' => __( 'Choose how customers select dates.', 'booking' ),
320 'fixed_days_invalid' => __( 'Enter a fixed range between 1 and 180 days.', 'booking' ),
321 'dynamic_days_invalid' => __( 'Enter a flexible range between 1 and 1095 days, with the maximum not less than the minimum.', 'booking' ),
322 'specific_days_invalid' => __( 'Enter comma-separated day lengths within the flexible range, for example 7,14,21,28.', 'booking' ),
323 ),
324 )
325 );
326 }
327
328 /**
329 * Resolve the current default booking resource through the canonical helper.
330 *
331 * @return int Positive resource ID.
332 */
333 private function get_preview_resource_id() {
334 $resource_id = class_exists( 'WPBC_FE_Attr_Postprocessor' )
335 ? absint( WPBC_FE_Attr_Postprocessor::get_default_booking_resource_id() )
336 : 1;
337
338 return $resource_id > 0 ? $resource_id : 1;
339 }
340
341 /**
342 * Read the allow-listed Customer Journey identifier from consumer context.
343 *
344 * @param array<string,mixed> $consumer_context Read-only dependency values.
345 *
346 * @return string Stable Customer Journey identifier.
347 */
348 private function get_customer_journey_id( array $consumer_context ) {
349 if (
350 isset( $consumer_context['customer_journey']['customer_journey'] )
351 && is_scalar( $consumer_context['customer_journey']['customer_journey'] )
352 ) {
353 return sanitize_key( (string) $consumer_context['customer_journey']['customer_journey'] );
354 }
355
356 return '';
357 }
358
359 /**
360 * Render a real, selectable two-month Booking Calendar preview.
361 *
362 * The renderer loads actual availability and bookings for the authorized
363 * default resource. Browser code changes only request-local selection
364 * parameters and never submits a booking or writes canonical settings.
365 *
366 * @param int $resource_id Authorized preview resource ID.
367 * @param array<string,string> $field_values Validated Date Selection draft.
368 * @param string $customer_journey_id Stable Customer Journey identifier.
369 *
370 * @return array{html:string,error:string} Preview result.
371 */
372 private function render_calendar_preview( $resource_id, array $field_values, $customer_journey_id ) {
373 if ( ! class_exists( 'WPBC_FE_Render' ) ) {
374 return array(
375 'html' => '',
376 'error' => __( 'The customer calendar preview is unavailable.', 'booking' ),
377 );
378 }
379
380 $preview_configuration = $this->date_selection->get_preview_configuration( $field_values, $customer_journey_id );
381 $request_overrides = $preview_configuration['calendar_request_overrides'];
382 // This page renders its configurable legend below the calendar, so suppress
383 // only the calendar renderer's duplicate legend in this local preview.
384 $request_overrides['wpbc_settings_calendar_preview_show_legend'] = 'Off';
385
386 $preview_html = WPBC_FE_Render::render_calendar_only(
387 array(
388 'resource_id' => $resource_id,
389 'cal_count' => 2,
390 'is_echo' => 0,
391 'shortcode_param__options' => '{calendar months_num_in_row=2 width=1023px}',
392 'calendar_request_overrides' => $request_overrides,
393 )
394 );
395
396 if ( ! is_string( $preview_html ) || '' === trim( $preview_html ) ) {
397 return array(
398 'html' => '',
399 'error' => __( 'The customer calendar preview could not be rendered.', 'booking' ),
400 );
401 }
402
403 return array(
404 'html' => $preview_html,
405 'error' => '',
406 );
407 }
408
409 /**
410 * Render canonical calendar-legend samples for the request-local preview.
411 *
412 * Each item is rendered separately so browser code can show, hide, and
413 * relabel an existing safe node without constructing or evaluating HTML.
414 * The saved Booking Calendar options remain unchanged.
415 *
416 * @param array<string,string> $field_values Validated Date Selection draft.
417 * @param int $resource_id Authorized preview resource ID.
418 *
419 * @return array<string,string> Canonical sample HTML keyed by legend item ID.
420 */
421 private function render_legend_preview_items( array $field_values, $resource_id ) {
422 $preview_items = array();
423 if ( ! function_exists( 'wpbc_get_calendar_legend__content_html' ) ) {
424 return $preview_items;
425 }
426
427 $show_numbers = isset( $field_values['date_selection_legend_show_numbers'] ) && 'On' === $field_values['date_selection_legend_show_numbers'];
428 $day_number = wp_date( 'd' );
429 foreach ( $this->date_selection->get_legend_items() as $legend_item ) {
430 $legend_id = $legend_item['id'];
431 $title_key = 'date_selection_legend_text_' . $legend_id;
432 $legend_title = isset( $field_values[ $title_key ] ) && '' !== $field_values[ $title_key ]
433 ? $field_values[ $title_key ]
434 : $legend_item['placeholder'];
435
436 $preview_items[ $legend_id ] = wpbc_get_calendar_legend__content_html(
437 array(
438 'is_vertical' => false,
439 'text_for_day_cell' => $show_numbers ? $day_number : '',
440 'items' => array( $legend_id ),
441 'resource_id' => absint( $resource_id ),
442 'titles' => array( $legend_id => $legend_title ),
443 )
444 );
445 }
446
447 return $preview_items;
448 }
449 }
450