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-review-setup / class-wpbc-setup-wizard-review-setup.php

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

1,024 lines 50.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Review-plan presentation for the Setup Wizard module.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Build a read-only, data-only summary of the validated Setup Wizard draft.
14 *
15 * This service deliberately does not apply canonical settings. It translates
16 * values already owned by earlier steps into a detailed review plan and keeps
17 * every Edit target tied to a server-registered step identifier.
18 */
19 final class WPBC_Setup_Wizard_Review_Setup {
20
21 /**
22 * Build the complete review context.
23 *
24 * @param array<string,mixed> $field_values Review-step field values.
25 * @param array<string,mixed> $consumer_context Validated values from earlier steps.
26 *
27 * @return array<string,mixed> Review presentation data.
28 */
29 public function get_context( array $field_values, array $consumer_context ) {
30 $journey_id = $this->get_scalar_value( $consumer_context, 'customer_journey', 'customer_journey' );
31 $journeys = WPBC_Setup_Wizard_Step_Data::get_customer_journeys();
32 $journey = isset( $journeys[ $journey_id ] ) ? $journeys[ $journey_id ] : reset( $journeys );
33 $feedback_profile = array();
34
35 if ( WPBC_Setup_Wizard_Environment_Policy::allows_business_details() ) {
36 $feedback_profile = $this->create_row(
37 'business_details',
38 __( 'Business details', 'booking' ),
39 $this->get_business_review( $consumer_context )
40 );
41 }
42
43 return array(
44 'values' => $field_values,
45 'journey' => is_array( $journey ) ? $journey : array(),
46 'groups' => $this->get_review_groups( $consumer_context, $journey_id ),
47 'feedback_profile' => $feedback_profile,
48 );
49 }
50
51 /**
52 * Return the ordered review groups for the current route.
53 *
54 * @param array<string,mixed> $consumer_context Validated values from earlier steps.
55 * @param string $journey_id Selected Customer Journey identifier.
56 *
57 * @return array<int,array{id:string,label:string,rows:array<int,array<string,mixed>>}> Review groups.
58 */
59 private function get_review_groups( array $consumer_context, $journey_id ) {
60 $general_rows = array();
61 $is_live_demo = WPBC_Setup_Wizard_Environment_Policy::is_live_demo();
62
63 $general_rows[] = $this->create_row(
64 'date_time_formats',
65 __( 'Dates & times', 'booking' ),
66 $this->get_date_time_review( $consumer_context )
67 );
68 $general_rows[] = $this->create_row(
69 'booking_experience',
70 __( 'Terminology', 'booking' ),
71 $this->get_booking_experience_review( $consumer_context )
72 );
73
74 $booking_rows = array();
75 if ( 'guided_appointment_flow' === $journey_id ) {
76 $booking_rows[] = $this->create_row( 'services', __( 'Services', 'booking' ), $this->get_services_review( $consumer_context ) );
77 }
78 if ( WPBC_Setup_Wizard_Customer_Journey_Policy::uses_booking_resources_configuration( $journey_id ) ) {
79 $booking_rows[] = $this->create_row( 'booking_resources', __( 'Booking resources', 'booking' ), $this->get_booking_resources_review( $consumer_context ) );
80 }
81
82 if ( WPBC_Setup_Wizard_Customer_Journey_Policy::uses_fixed_time_slots_configuration( $journey_id ) ) {
83 $booking_rows[] = $this->create_row( 'fixed_time_slots', __( 'Fixed time slots', 'booking' ), $this->get_fixed_time_slots_review( $consumer_context ) );
84 } elseif ( WPBC_Setup_Wizard_Customer_Journey_Policy::uses_start_duration_configuration( $journey_id ) ) {
85 $booking_rows[] = $this->create_row( 'start_duration_times', __( 'Start & duration times', 'booking' ), $this->get_start_duration_times_review( $consumer_context ) );
86 } elseif ( WPBC_Setup_Wizard_Customer_Journey_Policy::uses_start_end_configuration( $journey_id ) ) {
87 $booking_rows[] = $this->create_row( 'start_end_times', __( 'Start & end times', 'booking' ), $this->get_start_end_times_review( $consumer_context ) );
88 }
89
90 if ( WPBC_Setup_Wizard_Customer_Journey_Policy::uses_working_hours_configuration( $journey_id ) ) {
91 $booking_rows[] = $this->create_row( 'working_hours', __( 'Working hours', 'booking' ), $this->get_working_hours_review( $consumer_context ) );
92 }
93
94 $booking_rows[] = $this->create_row( 'days_off', __( 'Days off', 'booking' ), $this->get_days_off_review() );
95 $booking_rows[] = $this->create_row( 'date_selection', __( 'Date selection', 'booking' ), $this->get_date_selection_review( $consumer_context ) );
96
97 if ( WPBC_Setup_Wizard_Customer_Journey_Policy::uses_booking_form_and_appearance_route( $journey_id ) ) {
98 $booking_rows[] = $this->create_row( 'booking_form_template', __( 'Booking form', 'booking' ), $this->get_booking_form_review( $consumer_context, $journey_id ) );
99 $booking_rows[] = $this->create_row( 'appearance', __( 'Appearance', 'booking' ), $this->get_appearance_review( $consumer_context ) );
100 }
101
102 $groups = array(
103 array(
104 'id' => 'business',
105 'label' => __( 'General settings', 'booking' ),
106 'rows' => $general_rows,
107 ),
108 array(
109 'id' => 'booking_experience',
110 'label' => __( 'Booking experience', 'booking' ),
111 'rows' => $booking_rows,
112 ),
113 );
114
115 if ( ! $is_live_demo && isset( $consumer_context['publish_integration'] ) ) {
116 $groups[] = array(
117 'id' => 'publishing',
118 'label' => __( 'Publishing', 'booking' ),
119 'rows' => array(
120 $this->create_row(
121 'publish_integration',
122 __( 'Publish destination', 'booking' ),
123 $this->get_publish_review( $consumer_context )
124 ),
125 ),
126 );
127 }
128
129 return $groups;
130 }
131
132 /**
133 * Create one stable review row.
134 *
135 * @param string $step_id Step edited by this row.
136 * @param string $label Visible row label.
137 * @param array<string,mixed> $review Structured row presentation.
138 *
139 * @return array<string,mixed> Review row.
140 */
141 private function create_row( $step_id, $label, array $review ) {
142 $details = array();
143 foreach ( isset( $review['details'] ) && is_array( $review['details'] ) ? $review['details'] : array() as $detail ) {
144 if ( ! is_array( $detail ) || ! isset( $detail['label'], $detail['value'] ) ) {
145 continue;
146 }
147 $normalized_detail = array(
148 'label' => sanitize_text_field( (string) $detail['label'] ),
149 'value' => sanitize_text_field( (string) $detail['value'] ),
150 );
151 if ( ! empty( $detail['feedback_hidden'] ) ) {
152 $normalized_detail['feedback_hidden'] = true;
153 }
154 if ( isset( $detail['feedback_value'] ) && is_scalar( $detail['feedback_value'] ) ) {
155 $normalized_detail['feedback_value'] = sanitize_text_field( (string) $detail['feedback_value'] );
156 }
157 if ( isset( $detail['url'] ) && is_scalar( $detail['url'] ) ) {
158 $normalized_url = esc_url_raw( (string) $detail['url'] );
159 if ( '' !== $normalized_url ) {
160 $normalized_detail['url'] = $normalized_url;
161 }
162 }
163 $details[] = $normalized_detail;
164 }
165
166 $items = array();
167 foreach ( isset( $review['items'] ) && is_array( $review['items'] ) ? $review['items'] : array() as $item ) {
168 if ( ! is_array( $item ) || ! isset( $item['label'], $item['value'] ) ) {
169 continue;
170 }
171 $normalized_item = array(
172 'label' => sanitize_text_field( (string) $item['label'] ),
173 'value' => sanitize_text_field( (string) $item['value'] ),
174 );
175 if ( ! empty( $item['feedback_hidden'] ) ) {
176 $normalized_item['feedback_hidden'] = true;
177 }
178 if ( isset( $item['feedback_value'] ) && is_scalar( $item['feedback_value'] ) ) {
179 $normalized_item['feedback_value'] = sanitize_text_field( (string) $item['feedback_value'] );
180 }
181 $items[] = $normalized_item;
182 }
183
184 return array(
185 'step_id' => sanitize_key( $step_id ),
186 'label' => sanitize_text_field( $label ),
187 'summary' => isset( $review['summary'] ) ? sanitize_text_field( (string) $review['summary'] ) : '',
188 'details' => $details,
189 'items' => $items,
190 'items_label' => isset( $review['items_label'] ) ? sanitize_text_field( (string) $review['items_label'] ) : '',
191 );
192 }
193
194 /**
195 * Describe Business details for the explicitly consented feedback email.
196 *
197 * Business details intentionally remain absent from the visible Review plan.
198 * Keeping this record separate preserves the legacy feedback payload without
199 * reintroducing the intentionally omitted Review row.
200 *
201 * @param array<string,mixed> $consumer_context Earlier step values.
202 *
203 * @return array<string,mixed> Structured review presentation.
204 */
205 private function get_business_review( array $consumer_context ) {
206 $values = isset( $consumer_context['business_details'] ) && is_array( $consumer_context['business_details'] ) ? $consumer_context['business_details'] : array();
207 $step_data = new WPBC_Setup_Wizard_Step_Data();
208 $business_name = isset( $values['business_name'] ) ? sanitize_text_field( (string) $values['business_name'] ) : '';
209 $business_stage = isset( $values['business_stage'] ) ? sanitize_key( (string) $values['business_stage'] ) : '';
210 $industry_id = isset( $values['industry'] ) ? sanitize_key( (string) $values['industry'] ) : '';
211 $industry_label = $this->find_grouped_option_label( $step_data->get_industry_groups(), $industry_id, __( 'Not selected', 'booking' ) );
212 $business_stages = $step_data->get_business_stages();
213
214 return array(
215 'summary' => '' !== $business_name ? $business_name : __( 'Business profile', 'booking' ),
216 'details' => array(
217 $this->create_detail( __( 'Business name', 'booking' ), '' !== $business_name ? $business_name : __( 'Not configured', 'booking' ) ),
218 $this->create_detail( __( 'Business stage', 'booking' ), isset( $business_stages[ $business_stage ] ) ? $business_stages[ $business_stage ] : __( 'Not selected', 'booking' ) ),
219 $this->create_detail( __( 'Industry', 'booking' ), $industry_label ),
220 $this->create_detail( __( 'Booking email', 'booking' ), isset( $values['booking_email'] ) ? sanitize_email( (string) $values['booking_email'] ) : '' ),
221 $this->create_feedback_hidden_detail( __( 'Send setup details by email', 'booking' ), ! empty( $values['personalization_consent'] ) ? __( 'Enabled', 'booking' ) : __( 'Disabled', 'booking' ) ),
222 ),
223 );
224 }
225
226 /**
227 * Describe every selected date, time, language, and week-start choice.
228 *
229 * @param array<string,mixed> $consumer_context Earlier step values.
230 *
231 * @return array<string,mixed> Structured review presentation.
232 */
233 private function get_date_time_review( array $consumer_context ) {
234 $date_format = $this->get_scalar_value( $consumer_context, 'date_time_formats', 'date_format' );
235 $time_format = $this->get_scalar_value( $consumer_context, 'date_time_formats', 'time_format' );
236 $week_start = absint( $this->get_scalar_value( $consumer_context, 'date_time_formats', 'start_day_of_week' ) );
237 $weekdays = array(
238 __( 'Sunday', 'booking' ),
239 __( 'Monday', 'booking' ),
240 __( 'Tuesday', 'booking' ),
241 __( 'Wednesday', 'booking' ),
242 __( 'Thursday', 'booking' ),
243 __( 'Friday', 'booking' ),
244 __( 'Saturday', 'booking' ),
245 );
246 $sample_time = wp_date( in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ? $time_format : 'H:i', 48600, new DateTimeZone( 'UTC' ) );
247
248 $date_time_context = ( new WPBC_Setup_Wizard_Date_Time_Formats() )->get_context(
249 isset( $consumer_context['date_time_formats'] ) && is_array( $consumer_context['date_time_formats'] ) ? $consumer_context['date_time_formats'] : array()
250 );
251 $language = isset( $date_time_context['site_language'] ) && is_array( $date_time_context['site_language'] ) ? $date_time_context['site_language'] : array();
252 $sample_date = wp_date( '' !== $date_format ? $date_format : 'F j, Y', strtotime( '2027-03-24 12:00:00 UTC' ), new DateTimeZone( 'UTC' ) );
253
254 return array(
255 /* translators: 1: Example date, 2: Example time. */
256 'summary' => sprintf( __( '%1$s at %2$s', 'booking' ), $sample_date, $sample_time ),
257 'details' => array(
258 $this->create_feedback_hidden_detail( __( 'Date format', 'booking' ), $date_format ),
259 $this->create_feedback_hidden_detail( __( 'Time format', 'booking' ), $time_format ),
260 $this->create_detail( __( 'Week starts', 'booking' ), $weekdays[ min( 6, $week_start ) ] ),
261 $this->create_detail( __( 'Website language', 'booking' ), isset( $language['label'] ) ? $language['label'] : '' ),
262 $this->create_detail( __( 'Translation source', 'booking' ), isset( $language['translation_source_label'] ) ? $language['translation_source_label'] : '' ),
263 $this->create_feedback_hidden_detail( __( 'Translation status', 'booking' ), isset( $language['status_label'] ) ? $language['status_label'] : '' ),
264 ),
265 );
266 }
267
268 /**
269 * Describe the selected administration terminology mode.
270 *
271 * @param array<string,mixed> $consumer_context Earlier step values.
272 *
273 * @return array<string,mixed> Structured review presentation.
274 */
275 private function get_booking_experience_review( array $consumer_context ) {
276 $mode_id = $this->get_scalar_value( $consumer_context, 'booking_experience', 'booking_experience' );
277 $context = ( new WPBC_Setup_Wizard_Step_Data() )->get_step_context( 'booking_experience', $consumer_context );
278 $record = $this->find_record( isset( $context['experiences'] ) && is_array( $context['experiences'] ) ? $context['experiences'] : array(), 'id', $mode_id );
279
280 return array(
281 'summary' => isset( $record['title'] ) ? $record['title'] : __( 'Booking Calendar', 'booking' ),
282 'details' => array(
283 $this->create_detail( __( 'Administration label', 'booking' ), isset( $record['toolbar_label'] ) ? $record['toolbar_label'] : '' ),
284 $this->create_feedback_hidden_detail( __( 'Administration areas', 'booking' ), isset( $record['dashboard_uses'] ) ? $record['dashboard_uses'] : '' ),
285 $this->create_feedback_hidden_detail( __( 'Best for', 'booking' ), isset( $record['best_for'] ) ? $record['best_for'] : '' ),
286 ),
287 );
288 }
289
290 /**
291 * Describe every proposed Service.
292 *
293 * @param array<string,mixed> $consumer_context Earlier step values.
294 *
295 * @return array<string,mixed> Structured review presentation.
296 */
297 private function get_services_review( array $consumer_context ) {
298 $services = isset( $consumer_context['services']['services'] ) && is_array( $consumer_context['services']['services'] )
299 ? array_values( $consumer_context['services']['services'] )
300 : array();
301
302 if ( empty( $services ) ) {
303 return array( 'summary' => __( 'No Services selected', 'booking' ) );
304 }
305
306 $services_context = ( new WPBC_Setup_Wizard_Services() )->get_context( array( 'services' => $services ) );
307 $show_price = ! empty( $services_context['pricing_available'] );
308 $currency_symbol = isset( $services_context['currency_symbol'] ) ? (string) $services_context['currency_symbol'] : '$';
309 $items = array();
310
311 foreach ( $services as $service_index => $service ) {
312 $service = is_array( $service ) ? $service : array();
313 $service_title = isset( $service['title'] ) && '' !== trim( (string) $service['title'] ) ? (string) $service['title'] : sprintf( __( 'Service %d', 'booking' ), $service_index + 1 );
314 $parts = array();
315 /* translators: %d: Service duration in minutes. */
316 $parts[] = sprintf( __( '%d minutes', 'booking' ), isset( $service['duration_minutes'] ) ? absint( $service['duration_minutes'] ) : 0 );
317 /* translators: 1: Buffer before in minutes, 2: Buffer after in minutes. */
318 $parts[] = sprintf( __( 'Buffers: %1$d before, %2$d after', 'booking' ), isset( $service['buffer_before_minutes'] ) ? absint( $service['buffer_before_minutes'] ) : 0, isset( $service['buffer_after_minutes'] ) ? absint( $service['buffer_after_minutes'] ) : 0 );
319 if ( $show_price && isset( $service['base_cost'] ) && is_numeric( $service['base_cost'] ) ) {
320 $parts[] = sprintf( '%1$s%2$s', $currency_symbol, number_format_i18n( (float) $service['base_cost'], 2 ) );
321 }
322 $parts[] = isset( $service['status'] ) && 'active' === $service['status'] ? __( 'Active', 'booking' ) : __( 'Inactive', 'booking' );
323 $feedback_parts = $parts;
324 if ( ! empty( $service['description'] ) ) {
325 $parts[] = sprintf( __( 'Description: %s', 'booking' ), sanitize_textarea_field( (string) $service['description'] ) );
326 }
327 $image_state = ! empty( $service['picture_url'] ) ? __( 'Image selected', 'booking' ) : __( 'No image', 'booking' );
328 $parts[] = $image_state;
329 $feedback_parts[] = $image_state;
330 $items[] = $this->create_feedback_value_detail( $service_title, implode( ' · ', $parts ), implode( ' · ', $feedback_parts ) );
331 }
332
333 return array(
334 /* translators: %d: Number of Services. */
335 'summary' => sprintf( _n( '%d Service configured', '%d Services configured', count( $services ), 'booking' ), count( $services ) ),
336 'items' => $items,
337 'items_label' => __( 'Review configured Services', 'booking' ),
338 );
339 }
340
341 /**
342 * Describe Booking Resources created or reused by the full-day route.
343 *
344 * @param array<string,mixed> $consumer_context Earlier step values and verified results.
345 *
346 * @return array<string,mixed> Structured review presentation.
347 */
348 private function get_booking_resources_review( array $consumer_context ) {
349 $resource_drafts = isset( $consumer_context['booking_resources']['booking_resources'] ) && is_array( $consumer_context['booking_resources']['booking_resources'] )
350 ? array_values( $consumer_context['booking_resources']['booking_resources'] )
351 : array();
352 $items = array();
353
354 foreach ( $resource_drafts as $resource_index => $resource_draft ) {
355 $resource_draft = is_array( $resource_draft ) ? $resource_draft : array();
356 $title = isset( $resource_draft['title'] ) && '' !== trim( (string) $resource_draft['title'] )
357 ? (string) $resource_draft['title']
358 : sprintf( __( 'Booking Resource %d', 'booking' ), $resource_index + 1 );
359 $description = isset( $resource_draft['description'] ) ? sanitize_textarea_field( (string) $resource_draft['description'] ) : '';
360 $items[] = $this->create_detail( $title, '' !== $description ? $description : __( 'No description', 'booking' ) );
361 }
362
363 if ( empty( $items ) ) {
364 return array( 'summary' => __( 'Existing Booking Resources retained', 'booking' ) );
365 }
366
367 return array(
368 /* translators: %d: Number of Booking Resources created by Setup Wizard. */
369 'summary' => sprintf( _n( '%d Booking Resource created', '%d Booking Resources created', count( $items ), 'booking' ), count( $items ) ),
370 'items' => $items,
371 'items_label' => __( 'Review created Booking Resources', 'booking' ),
372 );
373 }
374
375 /**
376 * Describe the complete weekly Working Hours proposal.
377 *
378 * @param array<string,mixed> $consumer_context Earlier step values.
379 *
380 * @return array<string,mixed> Structured review presentation.
381 */
382 private function get_working_hours_review( array $consumer_context ) {
383 $working_hours = isset( $consumer_context['working_hours']['working_hours'] ) && is_array( $consumer_context['working_hours']['working_hours'] )
384 ? $consumer_context['working_hours']['working_hours']
385 : array();
386 $weekdays = isset( $working_hours['weekdays'] ) && is_array( $working_hours['weekdays'] ) ? $working_hours['weekdays'] : array();
387 $enabled_days = array_filter( $weekdays );
388 $time_format = $this->get_scalar_value( $consumer_context, 'date_time_formats', 'time_format' );
389 $time_format = in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ? $time_format : 'H:i';
390 $weekday_labels = array(
391 1 => __( 'Monday', 'booking' ),
392 2 => __( 'Tuesday', 'booking' ),
393 3 => __( 'Wednesday', 'booking' ),
394 4 => __( 'Thursday', 'booking' ),
395 5 => __( 'Friday', 'booking' ),
396 6 => __( 'Saturday', 'booking' ),
397 0 => __( 'Sunday', 'booking' ),
398 );
399 $items = array();
400
401 foreach ( $weekday_labels as $day_number => $weekday_label ) {
402 $intervals = isset( $weekdays[ $day_number ] ) && is_array( $weekdays[ $day_number ] ) ? $weekdays[ $day_number ] : array();
403 $labels = array();
404 foreach ( $intervals as $interval ) {
405 if ( ! is_array( $interval ) || ! isset( $interval['start_second'], $interval['end_second'] ) ) {
406 continue;
407 }
408 $labels[] = $this->format_time_second( $interval['start_second'], $time_format ) . '–' . $this->format_time_second( $interval['end_second'], $time_format );
409 }
410 $items[] = $this->create_detail( $weekday_label, ! empty( $labels ) ? implode( ', ', $labels ) : __( 'Closed', 'booking' ) );
411 }
412
413 return array(
414 /* translators: %d: Number of working days. */
415 'summary' => ! empty( $enabled_days ) ? sprintf( _n( '%d working day', '%d working days', count( $enabled_days ), 'booking' ), count( $enabled_days ) ) : __( 'No working hours selected', 'booking' ),
416 'details' => array(
417 $this->create_detail( __( 'Schedule status', 'booking' ), isset( $working_hours['enabled'] ) && 'On' === $working_hours['enabled'] ? __( 'Enabled', 'booking' ) : __( 'Disabled', 'booking' ) ),
418 ),
419 'items' => $items,
420 'items_label' => __( 'Review the weekly schedule', 'booking' ),
421 );
422 }
423
424 /**
425 * Describe the ordered start and end choices without expanding long lists.
426 *
427 * @param array<string,mixed> $consumer_context Earlier step values.
428 *
429 * @return array<string,mixed> Structured review presentation.
430 */
431 private function get_start_end_times_review( array $consumer_context ) {
432 $time_choices = isset( $consumer_context['start_end_times']['start_end_times'] ) && is_array( $consumer_context['start_end_times']['start_end_times'] )
433 ? $consumer_context['start_end_times']['start_end_times']
434 : array();
435 $start_times = isset( $time_choices['start_times'] ) && is_array( $time_choices['start_times'] ) ? array_values( $time_choices['start_times'] ) : array();
436 $end_times = isset( $time_choices['end_times'] ) && is_array( $time_choices['end_times'] ) ? array_values( $time_choices['end_times'] ) : array();
437 $time_format = $this->get_scalar_value( $consumer_context, 'date_time_formats', 'time_format' );
438 $time_format = in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ? $time_format : 'H:i';
439
440 return array(
441 /* translators: 1: Number of start-time choices, 2: Number of end-time choices. */
442 'summary' => sprintf( __( '%1$d start times / %2$d end times', 'booking' ), count( $start_times ), count( $end_times ) ),
443 'details' => array(
444 $this->create_detail( __( 'Start times', 'booking' ), $this->format_time_choice_summary( $start_times, $time_format ) ),
445 $this->create_detail( __( 'End times', 'booking' ), $this->format_time_choice_summary( $end_times, $time_format ) ),
446 ),
447 );
448 }
449
450 /**
451 * Describe ordered start-time and duration choices without long output.
452 *
453 * @param array<string,mixed> $consumer_context Earlier step values.
454 *
455 * @return array<string,mixed> Structured review presentation.
456 */
457 private function get_start_duration_times_review( array $consumer_context ) {
458 $time_choices = isset( $consumer_context['start_duration_times']['start_duration_times'] ) && is_array( $consumer_context['start_duration_times']['start_duration_times'] )
459 ? $consumer_context['start_duration_times']['start_duration_times']
460 : array();
461 $start_times = isset( $time_choices['start_times'] ) && is_array( $time_choices['start_times'] ) ? array_values( $time_choices['start_times'] ) : array();
462 $duration_times = isset( $time_choices['duration_times'] ) && is_array( $time_choices['duration_times'] ) ? array_values( $time_choices['duration_times'] ) : array();
463 $time_format = $this->get_scalar_value( $consumer_context, 'date_time_formats', 'time_format' );
464 $time_format = in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ? $time_format : 'H:i';
465 $duration_labels = array();
466 $duration_service = new WPBC_Setup_Wizard_Start_Duration_Times();
467 foreach ( array_slice( $duration_times, 0, 5 ) as $duration_time ) {
468 if ( ! is_scalar( $duration_time ) || ! preg_match( '/^(?:[01]\d|2[0-3]):[0-5]\d$|^24:00$/', (string) $duration_time ) ) {
469 continue;
470 }
471 list( $hour, $minute ) = array_map( 'intval', explode( ':', (string) $duration_time ) );
472 $duration_labels[] = $duration_service->format_duration_label( ( $hour * 60 ) + $minute );
473 }
474 $hidden_duration_count = count( $duration_times ) - count( $duration_labels );
475 if ( 0 < $hidden_duration_count ) {
476 /* translators: %d: Number of additional duration choices omitted from the compact summary. */
477 $duration_labels[] = sprintf( _n( '%d more duration', '%d more durations', $hidden_duration_count, 'booking' ), $hidden_duration_count );
478 }
479
480 return array(
481 /* translators: 1: Number of start-time choices, 2: Number of duration choices. */
482 'summary' => sprintf( __( '%1$d start times / %2$d durations', 'booking' ), count( $start_times ), count( $duration_times ) ),
483 'details' => array(
484 $this->create_detail( __( 'Start times', 'booking' ), $this->format_time_choice_summary( $start_times, $time_format ) ),
485 $this->create_detail( __( 'Durations', 'booking' ), ! empty( $duration_labels ) ? implode( ', ', $duration_labels ) : __( 'Not configured', 'booking' ) ),
486 ),
487 );
488 }
489
490 /**
491 * Describe ordered fixed time-slot ranges without expanding long lists.
492 *
493 * @param array<string,mixed> $consumer_context Earlier step values.
494 *
495 * @return array<string,mixed> Structured review presentation.
496 */
497 private function get_fixed_time_slots_review( array $consumer_context ) {
498 $fixed_time_slots = isset( $consumer_context['fixed_time_slots']['fixed_time_slots']['time_slots'] )
499 && is_array( $consumer_context['fixed_time_slots']['fixed_time_slots']['time_slots'] )
500 ? array_values( $consumer_context['fixed_time_slots']['fixed_time_slots']['time_slots'] )
501 : array();
502 $time_format = $this->get_scalar_value( $consumer_context, 'date_time_formats', 'time_format' );
503 $time_format = in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ? $time_format : 'H:i';
504 $slot_labels = array();
505 $slot_service = new WPBC_Setup_Wizard_Fixed_Time_Slots();
506
507 foreach ( array_slice( $fixed_time_slots, 0, 5 ) as $time_slot ) {
508 if ( ! is_array( $time_slot ) || ! isset( $time_slot['start_time'], $time_slot['end_time'] ) ) {
509 continue;
510 }
511 $start_label = $slot_service->format_time_label( (string) $time_slot['start_time'], $time_format );
512 $end_label = $slot_service->format_time_label( (string) $time_slot['end_time'], $time_format );
513 if ( '' !== $start_label && '' !== $end_label ) {
514 $slot_labels[] = $start_label . '–' . $end_label;
515 }
516 }
517
518 $hidden_slot_count = count( $fixed_time_slots ) - count( $slot_labels );
519 if ( 0 < $hidden_slot_count ) {
520 /* translators: %d: Number of additional fixed time slots omitted from the compact summary. */
521 $slot_labels[] = sprintf( _n( '%d more slot', '%d more slots', $hidden_slot_count, 'booking' ), $hidden_slot_count );
522 }
523
524 return array(
525 /* translators: %d: Number of fixed time slots. */
526 'summary' => sprintf( _n( '%d fixed time slot', '%d fixed time slots', count( $fixed_time_slots ), 'booking' ), count( $fixed_time_slots ) ),
527 'details' => array(
528 $this->create_detail( __( 'Time slots', 'booking' ), ! empty( $slot_labels ) ? implode( ', ', $slot_labels ) : __( 'Not configured', 'booking' ) ),
529 ),
530 );
531 }
532
533 /**
534 * Describe all current canonical unavailable-date ranges.
535 *
536 * @return array<string,mixed> Structured review presentation.
537 */
538 private function get_days_off_review() {
539 $state = ( new WPBC_Setup_Wizard_Days_Off() )->get_state();
540 if ( is_wp_error( $state ) || empty( $state['ranges'] ) || ! is_array( $state['ranges'] ) ) {
541 return array( 'summary' => __( 'No upcoming unavailable date periods', 'booking' ) );
542 }
543
544 $range_count = count( $state['ranges'] );
545 $items = array();
546 foreach ( $state['ranges'] as $range ) {
547 if ( ! is_array( $range ) || empty( $range['label'] ) ) {
548 continue;
549 }
550 $day_count = isset( $range['day_count'] ) ? absint( $range['day_count'] ) : 0;
551 $scope = isset( $range['scope_label'] ) ? sanitize_text_field( (string) $range['scope_label'] ) : '';
552 /* translators: 1: Number of unavailable days, 2: Booking-item scope. */
553 $description = sprintf( _n( '%1$d day · %2$s', '%1$d days · %2$s', $day_count, 'booking' ), $day_count, $scope );
554 $items[] = $this->create_detail( $range['label'], $description );
555 }
556
557 return array(
558 /* translators: %d: Number of unavailable date periods. */
559 'summary' => sprintf( _n( '%d unavailable date period', '%d unavailable date periods', $range_count, 'booking' ), $range_count ),
560 'items' => $items,
561 'items_label' => __( 'Review unavailable date periods', 'booking' ),
562 );
563 }
564
565 /**
566 * Describe the complete customer-facing Date Selection policy.
567 *
568 * @param array<string,mixed> $consumer_context Earlier step values.
569 *
570 * @return array<string,mixed> Structured review presentation.
571 */
572 private function get_date_selection_review( array $consumer_context ) {
573 $values = isset( $consumer_context['date_selection'] ) && is_array( $consumer_context['date_selection'] ) ? $consumer_context['date_selection'] : array();
574 $mode_id = $this->get_scalar_value( $consumer_context, 'date_selection', 'date_selection_mode' );
575 $labels = array(
576 'single' => __( 'Single day', 'booking' ),
577 'multiple' => __( 'Multiple days', 'booking' ),
578 'dynamic' => __( 'Flexible range', 'booking' ),
579 'fixed' => __( 'Fixed range', 'booking' ),
580 );
581
582 $details = array();
583 if ( 'fixed' === $mode_id ) {
584 $details[] = $this->create_detail( __( 'Number of selected days', 'booking' ), isset( $values['date_selection_fixed_days'] ) ? absint( $values['date_selection_fixed_days'] ) : 0 );
585 $details[] = $this->create_detail( __( 'Booking can start on', 'booking' ), $this->format_weekday_selection( isset( $values['date_selection_fixed_weekdays'] ) ? $values['date_selection_fixed_weekdays'] : '' ) );
586 } elseif ( 'dynamic' === $mode_id ) {
587 $details[] = $this->create_detail( __( 'Minimum days', 'booking' ), isset( $values['date_selection_dynamic_min'] ) ? absint( $values['date_selection_dynamic_min'] ) : 0 );
588 $details[] = $this->create_detail( __( 'Maximum days', 'booking' ), isset( $values['date_selection_dynamic_max'] ) ? absint( $values['date_selection_dynamic_max'] ) : 0 );
589 $details[] = $this->create_detail( __( 'Specific selectable lengths', 'booking' ), ! empty( $values['date_selection_dynamic_specific'] ) ? $values['date_selection_dynamic_specific'] : __( 'Any length within the range', 'booking' ) );
590 $details[] = $this->create_detail( __( 'Booking can start on', 'booking' ), $this->format_weekday_selection( isset( $values['date_selection_dynamic_weekdays'] ) ? $values['date_selection_dynamic_weekdays'] : '' ) );
591 }
592
593 $changeover_enabled = isset( $values['date_selection_changeover_enabled'] ) && 'On' === $values['date_selection_changeover_enabled'];
594 $details[] = $this->create_detail( __( 'Changeover days', 'booking' ), $this->format_toggle( $changeover_enabled ) );
595 if ( $changeover_enabled ) {
596 $details[] = $this->create_detail( __( 'Check-in time', 'booking' ), isset( $values['date_selection_check_in_time'] ) ? $values['date_selection_check_in_time'] : '' );
597 $details[] = $this->create_detail( __( 'Check-out time', 'booking' ), isset( $values['date_selection_check_out_time'] ) ? $values['date_selection_check_out_time'] : '' );
598 $details[] = $this->create_detail( __( 'Show changeover days as triangles', 'booking' ), $this->format_toggle( isset( $values['date_selection_triangles'] ) && 'On' === $values['date_selection_triangles'] ) );
599 }
600 $details[] = $this->create_detail( __( 'Use selected times for each booking date', 'booking' ), $this->format_toggle( isset( $values['date_selection_recurrent_time'] ) && 'On' === $values['date_selection_recurrent_time'] ) );
601 $details[] = $this->create_detail( __( 'Set check-out date as available', 'booking' ), $this->format_toggle( isset( $values['date_selection_checkout_available'] ) && 'On' === $values['date_selection_checkout_available'] ) );
602 $details[] = $this->create_detail( __( 'Calendar legend', 'booking' ), $this->format_toggle( isset( $values['date_selection_legend_enabled'] ) && 'On' === $values['date_selection_legend_enabled'] ) );
603
604 $legend_items = array();
605 foreach ( ( new WPBC_Setup_Wizard_Date_Selection() )->get_legend_items() as $legend_item ) {
606 $legend_id = $legend_item['id'];
607 $show_key = 'date_selection_legend_item_' . $legend_id;
608 $text_key = 'date_selection_legend_text_' . $legend_id;
609 $legend_text = ! empty( $values[ $text_key ] ) ? $values[ $text_key ] : $legend_item['placeholder'];
610 $legend_items[] = $this->create_detail(
611 $legend_item['label'],
612 isset( $values[ $show_key ] ) && 'On' === $values[ $show_key ]
613 ? sprintf( __( 'Shown as “%s”', 'booking' ), $legend_text )
614 : __( 'Hidden', 'booking' )
615 );
616 }
617 $legend_items[] = $this->create_detail( __( 'Show date number', 'booking' ), $this->format_toggle( isset( $values['date_selection_legend_show_numbers'] ) && 'On' === $values['date_selection_legend_show_numbers'] ) );
618 $legend_items[] = $this->create_detail( __( 'Legend layout', 'booking' ), isset( $values['date_selection_legend_vertical'] ) && 'On' === $values['date_selection_legend_vertical'] ? __( 'Column', 'booking' ) : __( 'Row', 'booking' ) );
619
620 return array(
621 'summary' => isset( $labels[ $mode_id ] ) ? $labels[ $mode_id ] : __( 'Date selection', 'booking' ),
622 'details' => $details,
623 'items' => $legend_items,
624 'items_label' => __( 'Review Calendar legend items', 'booking' ),
625 );
626 }
627
628 /**
629 * Summarize the selected Booking Form template and entry point.
630 *
631 * @param array<string,mixed> $consumer_context Earlier step values.
632 * @param string $journey_id Selected Customer Journey ID.
633 *
634 * @return array<string,mixed> Structured review presentation.
635 */
636 private function get_booking_form_review( array $consumer_context, $journey_id ) {
637 $values = isset( $consumer_context['booking_form_template'] ) && is_array( $consumer_context['booking_form_template'] ) ? $consumer_context['booking_form_template'] : array();
638 $context = ( new WPBC_Setup_Wizard_Booking_Form_Templates() )->get_context( $values, $journey_id );
639 $template = isset( $context['selected_template'] ) && is_array( $context['selected_template'] ) ? $context['selected_template'] : array();
640 $title = isset( $template['title'] ) ? sanitize_text_field( (string) $template['title'] ) : __( 'Booking form', 'booking' );
641 $usage = isset( $values['booking_form_usage'] ) ? sanitize_key( (string) $values['booking_form_usage'] ) : '';
642 $usage_record = $this->find_record( isset( $context['usage_options'] ) && is_array( $context['usage_options'] ) ? $context['usage_options'] : array(), 'id', $usage );
643 $usage_label = isset( $usage_record['label'] ) ? $usage_record['label'] : __( 'Direct booking form', 'booking' );
644
645 return array(
646 'summary' => $title,
647 'details' => array(
648 $this->create_detail( __( 'Entry point', 'booking' ), $usage_label ),
649 $this->create_detail( __( 'Entry-point behavior', 'booking' ), isset( $usage_record['description'] ) ? $usage_record['description'] : '' ),
650 $this->create_detail( __( 'Template category', 'booking' ), isset( $template['category'] ) ? $template['category'] : '' ),
651 $this->create_feedback_hidden_detail( __( 'Template description', 'booking' ), isset( $template['description'] ) ? $template['description'] : '' ),
652 ),
653 );
654 }
655
656 /**
657 * Summarize the selected appearance style and accent.
658 *
659 * @param array<string,mixed> $consumer_context Earlier step values.
660 *
661 * @return array<string,mixed> Structured review presentation.
662 */
663 private function get_appearance_review( array $consumer_context ) {
664 $values = isset( $consumer_context['appearance'] ) && is_array( $consumer_context['appearance'] ) ? $consumer_context['appearance'] : array();
665 $appearance = new WPBC_Setup_Wizard_Appearance();
666 $style_id = isset( $values['booking_form_style'] ) ? sanitize_key( (string) $values['booking_form_style'] ) : '';
667 $accent = isset( $values['booking_form_accent_color'] ) ? sanitize_hex_color( (string) $values['booking_form_accent_color'] ) : '';
668 $style_label = $this->find_record_label( $appearance->get_starting_styles(), 'id', $style_id, __( 'Booking appearance', 'booking' ) );
669 $accent_label = $this->find_record_label( $appearance->get_accent_colors(), 'value', $accent, $accent );
670 $skin_value = isset( $values['booking_skin'] ) ? sanitize_text_field( (string) $values['booking_skin'] ) : '';
671 $skin_label = $this->find_grouped_record_label( $appearance->get_calendar_skin_groups(), 'value', $skin_value, $skin_value );
672
673 return array(
674 'summary' => $style_label,
675 'details' => array(
676 $this->create_detail( __( 'Accent color', 'booking' ), trim( $accent_label . ( '' !== $accent ? ' (' . $accent . ')' : '' ) ) ),
677 $this->create_detail( __( 'Calendar skin', 'booking' ), $skin_label ),
678 $this->create_detail( __( 'Time choices', 'booking' ), isset( $values['booking_timeslot_picker'] ) && 'On' === $values['booking_timeslot_picker'] ? __( 'Time-slot picker', 'booking' ) : __( 'Standard time choices', 'booking' ) ),
679 ),
680 );
681 }
682
683 /**
684 * Summarize the saved publishing destination and verified operation result.
685 *
686 * @param array<string,mixed> $consumer_context Earlier step values.
687 * @return array<string,mixed> Structured review presentation.
688 */
689 private function get_publish_review( array $consumer_context ) {
690 $values = isset( $consumer_context['publish_integration'] ) && is_array( $consumer_context['publish_integration'] ) ? $consumer_context['publish_integration'] : array();
691 $step_results = isset( $consumer_context['_step_results'] ) && is_array( $consumer_context['_step_results'] ) ? $consumer_context['_step_results'] : array();
692 $publish_result = isset( $step_results['publish_integration'] ) && is_array( $step_results['publish_integration'] ) ? $step_results['publish_integration'] : array();
693 $destination = isset( $values['publish_destination'] ) ? sanitize_key( (string) $values['publish_destination'] ) : '';
694 $labels = array(
695 WPBC_Setup_Wizard_Publish_Integration::DESTINATION_CREATE_PAGE => __( 'Create a new page', 'booking' ),
696 WPBC_Setup_Wizard_Publish_Integration::DESTINATION_EXISTING_PAGE => __( 'Use an existing page', 'booking' ),
697 WPBC_Setup_Wizard_Publish_Integration::DESTINATION_MANUAL => __( 'Add it manually', 'booking' ),
698 WPBC_Setup_Wizard_Publish_Integration::DESTINATION_LATER => __( 'Decide later', 'booking' ),
699 );
700 $service = new WPBC_Setup_Wizard_Publish_Integration();
701 $journey_id = $this->get_scalar_value( $consumer_context, 'customer_journey', 'customer_journey' );
702 $form_slug = $this->get_scalar_value( $consumer_context, 'booking_form_template', 'booking_form_template' );
703 $content_options = $service->get_content_options( $journey_id, $form_slug );
704 $content_id = isset( $values['publish_page_content'] ) ? sanitize_key( (string) $values['publish_page_content'] ) : '';
705 if ( WPBC_Setup_Wizard_Publish_Integration::CONTENT_RECOMMENDED === $content_id ) {
706 $content_id = $service->get_recommended_content_id( $journey_id );
707 }
708 $content_record = $this->find_record( $content_options, 'id', $content_id );
709 $page_title = isset( $values['publish_page_title'] ) ? sanitize_text_field( (string) $values['publish_page_title'] ) : '';
710 $page_id = isset( $values['publish_existing_page_id'] ) ? absint( $values['publish_existing_page_id'] ) : 0;
711 $page_label = '';
712 $page_url = '';
713
714 if ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_CREATE_PAGE === $destination ) {
715 $page_id = $this->get_result_page_id( $publish_result, 'wp_page_created:' );
716 if ( $page_id && current_user_can( 'edit_post', $page_id ) ) {
717 $permalink = get_permalink( $page_id );
718 $page_label = sanitize_text_field( get_the_title( $page_id ) );
719 $page_url = is_string( $permalink ) ? esc_url_raw( $permalink ) : '';
720 } else {
721 $page_label = $page_title;
722 $page_url = $service->get_new_page_url_preview( $page_title );
723 }
724 } elseif ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_EXISTING_PAGE === $destination && $page_id && current_user_can( 'edit_post', $page_id ) ) {
725 $permalink = get_permalink( $page_id );
726 $page_label = sanitize_text_field( get_the_title( $page_id ) );
727 $page_url = is_string( $permalink ) ? esc_url_raw( $permalink ) : '';
728 }
729
730 $details = array(
731 $this->create_detail( __( 'WordPress page', 'booking' ), '' !== $page_label ? $page_label : __( 'No page selected', 'booking' ) ),
732 $this->create_link_detail( __( 'Page URL', 'booking' ), $page_url ),
733 $this->create_detail( __( 'Page content', 'booking' ), isset( $content_record['label'] ) ? $content_record['label'] : __( 'Not applicable', 'booking' ) ),
734 $this->create_detail( __( 'Booking shortcode', 'booking' ), isset( $content_record['shortcode'] ) ? $content_record['shortcode'] : __( 'Not applicable', 'booking' ) ),
735 );
736 if ( isset( $publish_result['summary'] ) && is_scalar( $publish_result['summary'] ) && '' !== trim( (string) $publish_result['summary'] ) ) {
737 $details[] = $this->create_detail( __( 'Save result', 'booking' ), (string) $publish_result['summary'] );
738 }
739
740 return array(
741 'summary' => isset( $labels[ $destination ] ) ? $labels[ $destination ] : __( 'Publishing plan', 'booking' ),
742 'details' => $details,
743 );
744 }
745
746 /**
747 * Resolve a verified WordPress page ID from one normalized save result.
748 *
749 * @param array<string,mixed> $step_result Normalized progressive-save result.
750 * @param string $id_prefix Expected server-owned identifier prefix.
751 *
752 * @return int Verified result page ID, or zero when absent.
753 */
754 private function get_result_page_id( array $step_result, $id_prefix ) {
755 $result_ids = array_merge(
756 isset( $step_result['created_ids'] ) && is_array( $step_result['created_ids'] ) ? $step_result['created_ids'] : array(),
757 isset( $step_result['updated_ids'] ) && is_array( $step_result['updated_ids'] ) ? $step_result['updated_ids'] : array()
758 );
759 foreach ( $result_ids as $result_id ) {
760 if ( ! is_scalar( $result_id ) || 0 !== strpos( (string) $result_id, $id_prefix ) ) {
761 continue;
762 }
763
764 return absint( substr( (string) $result_id, strlen( $id_prefix ) ) );
765 }
766
767 return 0;
768 }
769
770 /**
771 * Create one label/value presentation record.
772 *
773 * @param mixed $label Visible detail label.
774 * @param mixed $value Visible detail value.
775 *
776 * @return array{label:string,value:string} Presentation record.
777 */
778 private function create_detail( $label, $value ) {
779 return array(
780 'label' => is_scalar( $label ) ? (string) $label : '',
781 'value' => is_scalar( $value ) ? (string) $value : '',
782 );
783 }
784
785 /**
786 * Create one label/value presentation record with an optional safe URL.
787 *
788 * The visible value remains available to the plain-text setup summary while
789 * the Review template may render the same authorized URL as an external link.
790 * Empty or invalid URLs intentionally fall back to ordinary text.
791 *
792 * @param mixed $label Visible detail label.
793 * @param mixed $url Candidate destination URL and visible value.
794 *
795 * @return array{label:string,value:string,url?:string} Link presentation record.
796 */
797 private function create_link_detail( $label, $url ) {
798 $detail = $this->create_detail( $label, $url );
799 $normalized_url = is_scalar( $url ) ? esc_url_raw( (string) $url ) : '';
800
801 if ( '' !== $normalized_url ) {
802 $detail['url'] = $normalized_url;
803 }
804
805 return $detail;
806 }
807
808 /**
809 * Create a visible detail with a shorter feedback-email value.
810 *
811 * The Review page retains the complete value while the explicitly consented
812 * feedback email omits long free-text descriptions.
813 *
814 * @param mixed $label Visible detail label.
815 * @param mixed $value Complete visible value.
816 * @param mixed $feedback_value Compact value used only by feedback email.
817 *
818 * @return array{label:string,value:string,feedback_value:string} Presentation record.
819 */
820 private function create_feedback_value_detail( $label, $value, $feedback_value ) {
821 $detail = $this->create_detail( $label, $value );
822 $detail['feedback_value'] = is_scalar( $feedback_value ) ? (string) $feedback_value : '';
823
824 return $detail;
825 }
826
827 /**
828 * Create a visible detail that is intentionally omitted from feedback email.
829 *
830 * @param mixed $label Visible detail label.
831 * @param mixed $value Complete visible value.
832 *
833 * @return array{label:string,value:string,feedback_hidden:bool} Presentation record.
834 */
835 private function create_feedback_hidden_detail( $label, $value ) {
836 $detail = $this->create_detail( $label, $value );
837 $detail['feedback_hidden'] = true;
838
839 return $detail;
840 }
841
842 /**
843 * Return a localized On or Off label.
844 *
845 * @param bool $is_enabled Whether the reviewed behavior is enabled.
846 *
847 * @return string Localized state label.
848 */
849 private function format_toggle( $is_enabled ) {
850 return $is_enabled ? __( 'On', 'booking' ) : __( 'Off', 'booking' );
851 }
852
853 /**
854 * Format one seconds-from-midnight value with the selected time format.
855 *
856 * @param mixed $raw_second Seconds from midnight.
857 * @param string $time_format Validated WordPress time format.
858 *
859 * @return string Formatted time.
860 */
861 private function format_time_second( $raw_second, $time_format ) {
862 $second = min( DAY_IN_SECONDS, absint( $raw_second ) );
863 if ( DAY_IN_SECONDS === $second ) {
864 return __( '24:00', 'booking' );
865 }
866
867 return wp_date( $time_format, $second, new DateTimeZone( 'UTC' ) );
868 }
869
870 /**
871 * Format a bounded preview of one ordered `HH:MM` choice list.
872 *
873 * @param string[] $time_values Validated time values.
874 * @param string $time_format Validated WordPress time format.
875 *
876 * @return string Localized compact list summary.
877 */
878 private function format_time_choice_summary( array $time_values, $time_format ) {
879 $visible_values = array_slice( $time_values, 0, 5 );
880 $labels = array();
881
882 foreach ( $visible_values as $time_value ) {
883 if ( ! is_scalar( $time_value ) || ! preg_match( '/^(?:[01]\d|2[0-3]):[0-5]\d$/', (string) $time_value ) ) {
884 continue;
885 }
886 list( $hour, $minute ) = array_map( 'intval', explode( ':', (string) $time_value ) );
887 $labels[] = wp_date( $time_format, ( ( $hour * 60 ) + $minute ) * MINUTE_IN_SECONDS, new DateTimeZone( 'UTC' ) );
888 }
889
890 $hidden_count = count( $time_values ) - count( $visible_values );
891 if ( 0 < $hidden_count ) {
892 /* translators: %d: Number of additional time choices omitted from the compact summary. */
893 $labels[] = sprintf( _n( '%d more time', '%d more times', $hidden_count, 'booking' ), $hidden_count );
894 }
895
896 return ! empty( $labels ) ? implode( ', ', $labels ) : __( 'Not configured', 'booking' );
897 }
898
899 /**
900 * Format a stored weekday list through the Date Selection registry.
901 *
902 * @param mixed $raw_weekdays Comma-separated weekday numbers or `-1`.
903 *
904 * @return string Localized weekday summary.
905 */
906 private function format_weekday_selection( $raw_weekdays ) {
907 $weekday_records = ( new WPBC_Setup_Wizard_Date_Selection() )->get_weekdays();
908 $weekday_labels = array();
909 foreach ( $weekday_records as $weekday_record ) {
910 if ( isset( $weekday_record['value'], $weekday_record['label'] ) ) {
911 $weekday_labels[ (string) $weekday_record['value'] ] = (string) $weekday_record['label'];
912 }
913 }
914 $weekday_ids = is_scalar( $raw_weekdays ) ? array_filter( array_map( 'trim', explode( ',', (string) $raw_weekdays ) ), 'strlen' ) : array();
915 if ( in_array( '-1', $weekday_ids, true ) || empty( $weekday_ids ) ) {
916 return __( 'Any day', 'booking' );
917 }
918 $selected_labels = array();
919 foreach ( $weekday_ids as $weekday_id ) {
920 if ( isset( $weekday_labels[ $weekday_id ] ) ) {
921 $selected_labels[] = $weekday_labels[ $weekday_id ];
922 }
923 }
924
925 return ! empty( $selected_labels ) ? implode( ', ', $selected_labels ) : __( 'Any day', 'booking' );
926 }
927
928 /**
929 * Find one complete record in a trusted presentation registry.
930 *
931 * @param array<int,array<string,mixed>> $records Presentation records.
932 * @param string $key Identity field.
933 * @param string $expected Expected identity.
934 *
935 * @return array<string,mixed> Matching record or an empty array.
936 */
937 private function find_record( array $records, $key, $expected ) {
938 foreach ( $records as $record ) {
939 if ( is_array( $record ) && isset( $record[ $key ] ) && (string) $record[ $key ] === (string) $expected ) {
940 return $record;
941 }
942 }
943
944 return array();
945 }
946
947 /**
948 * Find an associative option label in grouped presentation records.
949 *
950 * @param array<int,array<string,mixed>> $groups Group records.
951 * @param string $expected Expected option value.
952 * @param string $fallback_label Label used when no option matches.
953 *
954 * @return string Matching label or fallback.
955 */
956 private function find_grouped_option_label( array $groups, $expected, $fallback_label ) {
957 foreach ( $groups as $group ) {
958 if ( isset( $group['options'][ $expected ] ) && is_scalar( $group['options'][ $expected ] ) ) {
959 return sanitize_text_field( (string) $group['options'][ $expected ] );
960 }
961 }
962
963 return sanitize_text_field( (string) $fallback_label );
964 }
965
966 /**
967 * Find a label in grouped ordered presentation records.
968 *
969 * @param array<int,array<string,mixed>> $groups Group records.
970 * @param string $key Identity field.
971 * @param string $expected Expected option value.
972 * @param string $fallback_label Label used when no option matches.
973 *
974 * @return string Matching label or fallback.
975 */
976 private function find_grouped_record_label( array $groups, $key, $expected, $fallback_label ) {
977 foreach ( $groups as $group ) {
978 $record = $this->find_record( isset( $group['options'] ) && is_array( $group['options'] ) ? $group['options'] : array(), $key, $expected );
979 if ( isset( $record['label'] ) ) {
980 return sanitize_text_field( (string) $record['label'] );
981 }
982 }
983
984 return sanitize_text_field( (string) $fallback_label );
985 }
986
987 /**
988 * Find one translated label in a trusted presentation registry.
989 *
990 * @param array<int,array<string,mixed>> $records Presentation records.
991 * @param string $key Identity field.
992 * @param string $expected Expected identity.
993 * @param string $fallback_label Label used when no record matches.
994 *
995 * @return string Matching label or the fallback.
996 */
997 private function find_record_label( array $records, $key, $expected, $fallback_label ) {
998 foreach ( $records as $record ) {
999 if ( isset( $record[ $key ], $record['label'] ) && (string) $record[ $key ] === (string) $expected ) {
1000 return sanitize_text_field( (string) $record['label'] );
1001 }
1002 }
1003
1004 return sanitize_text_field( (string) $fallback_label );
1005 }
1006
1007 /**
1008 * Read one scalar dependency value.
1009 *
1010 * @param array<string,mixed> $consumer_context Dependency values keyed by step.
1011 * @param string $step_id Dependency step identifier.
1012 * @param string $field_id Field identifier.
1013 *
1014 * @return string Sanitized scalar value or an empty string.
1015 */
1016 private function get_scalar_value( array $consumer_context, $step_id, $field_id ) {
1017 if ( ! isset( $consumer_context[ $step_id ][ $field_id ] ) || ! is_scalar( $consumer_context[ $step_id ][ $field_id ] ) ) {
1018 return '';
1019 }
1020
1021 return sanitize_text_field( (string) $consumer_context[ $step_id ][ $field_id ] );
1022 }
1023 }
1024