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

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

544 lines 21.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Completed Setup Wizard overview presentation service.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Build the read-only, data-only Setup overview shown after completion.
14 *
15 * The checkpoint remains authoritative only for wizard-owned choices such as
16 * Customer Journey and the selected Booking Form template. Settings-backed
17 * summaries are supplied by the existing Review presenter, whose dependencies
18 * refresh canonical values before this mapper runs. Service and Provider totals
19 * are loaded from the owner-aware Appointment Services repository in one
20 * bounded query. This class never mutates configuration.
21 */
22 final class WPBC_Setup_Wizard_Setup_Overview {
23
24 /**
25 * Build the complete template context for one completed checkpoint.
26 *
27 * @param array<string,mixed> $checkpoint Authorized completed checkpoint.
28 * @param array<string,mixed> $review_context Current Review presenter context.
29 *
30 * @return array<string,mixed> JSON-safe overview presentation data.
31 */
32 public function get_context( array $checkpoint, array $review_context ) {
33 $review_rows = $this->index_review_rows( $review_context );
34 $service_counts = $this->get_service_counts();
35 $published_pages = $this->get_published_pages( $checkpoint );
36 $publishing_allowed = ( new WPBC_Setup_Wizard_Publish_Integration() )->is_available();
37 $publishing_url = $publishing_allowed ? $this->get_publishing_url() : '';
38 $journey = isset( $review_context['journey'] ) && is_array( $review_context['journey'] )
39 ? $review_context['journey']
40 : array();
41
42 $groups = array(
43 array(
44 'number' => 1,
45 'title' => __( 'What customers book', 'booking' ),
46 'description' => __( 'Services, Providers, and the booking flow.', 'booking' ),
47 'rows' => array(
48 $this->create_wizard_row(
49 'customer_journey',
50 'wpbc_icn_group',
51 __( 'Customer journey', 'booking' ),
52 isset( $journey['title'] ) ? (string) $journey['title'] : __( 'Not configured', 'booking' ),
53 __( 'Review', 'booking' )
54 ),
55 $this->create_link_row(
56 'services',
57 'wpbc_icn_format_list_bulleted',
58 __( 'Services', 'booking' ),
59 $this->format_service_count( $service_counts['services'] ),
60 $this->get_services_url(),
61 __( 'Review', 'booking' )
62 ),
63 $this->create_link_row(
64 'providers',
65 'wpbc_icn_groups',
66 __( 'Providers', 'booking' ),
67 $this->format_provider_count( $service_counts['providers'] ),
68 $this->get_providers_url(),
69 __( 'Review', 'booking' )
70 ),
71 ),
72 ),
73 array(
74 'number' => 2,
75 'title' => __( 'When they can book', 'booking' ),
76 'description' => __( 'Availability, working hours, and date selection.', 'booking' ),
77 'rows' => array(
78 $this->create_link_row( 'working_hours', 'wpbc_icn_schedule', __( 'Working hours', 'booking' ), $this->get_review_summary( $review_rows, 'working_hours' ), $this->get_working_hours_url(), __( 'Change', 'booking' ) ),
79 $this->create_link_row( 'days_off', 'wpbc_icn_event_busy', __( 'Days off', 'booking' ), $this->get_review_summary( $review_rows, 'days_off' ), $this->get_days_off_url(), __( 'Change', 'booking' ) ),
80 $this->create_link_row( 'date_selection', 'wpbc_icn_calendar_month', __( 'Date selection', 'booking' ), $this->get_review_summary( $review_rows, 'date_selection' ), $this->get_date_selection_url(), __( 'Change', 'booking' ) ),
81 ),
82 ),
83 array(
84 'number' => 3,
85 'title' => __( 'What customers see', 'booking' ),
86 'description' => __( 'The booking form and appearance on your site.', 'booking' ),
87 'rows' => array(
88 $this->create_link_row( 'booking_form_template', 'wpbc_icn_list_alt', __( 'Booking form', 'booking' ), $this->get_booking_form_summary( $review_rows ), $this->get_booking_form_url(), __( 'Review', 'booking' ) ),
89 $this->create_link_row( 'appearance', 'wpbc_icn_brush', __( 'Appearance', 'booking' ), $this->get_review_summary( $review_rows, 'appearance' ), $this->get_appearance_url(), __( 'Change', 'booking' ) ),
90 ),
91 ),
92 array(
93 'number' => 4,
94 'title' => __( 'Go live', 'booking' ),
95 'description' => __( 'Publish your booking form and start taking bookings.', 'booking' ),
96 'rows' => array(
97 $publishing_allowed
98 ? $this->create_link_row(
99 'publish_integration',
100 'wpbc_icn_open_in_new',
101 __( 'Publishing', 'booking' ),
102 $this->get_publishing_summary( $published_pages ),
103 $publishing_url,
104 __( 'Publish', 'booking' ),
105 empty( $published_pages ) ? 'danger' : 'success'
106 )
107 : $this->create_disabled_row( 'publish_integration', 'wpbc_icn_open_in_new', __( 'Publishing', 'booking' ), __( 'Unavailable in this environment', 'booking' ) ),
108 $this->create_link_row( 'notifications', 'wpbc_icn_email', __( 'Notifications', 'booking' ), __( 'Booking email notifications', 'booking' ), $this->get_notifications_url(), __( 'Change', 'booking' ) ),
109 ),
110 ),
111 );
112
113 return array(
114 'checkpoint' => array(
115 'current_step' => isset( $checkpoint['current_step'] ) ? sanitize_key( (string) $checkpoint['current_step'] ) : 'review_setup',
116 'revision' => isset( $checkpoint['revision'] ) ? absint( $checkpoint['revision'] ) : 0,
117 ),
118 'groups' => $groups,
119 'published_pages' => $published_pages,
120 'publishing_allowed' => $publishing_allowed,
121 'publishing_url' => $publishing_url,
122 'published_page_count' => count( $published_pages ),
123 'admin_post_url' => admin_url( 'admin-post.php' ),
124 'notice' => $this->get_request_notice(),
125 );
126 }
127
128 /**
129 * Flatten Review groups into a stable step-indexed lookup.
130 *
131 * @param array<string,mixed> $review_context Current Review presenter context.
132 *
133 * @return array<string,array<string,mixed>> Rows keyed by step ID.
134 */
135 private function index_review_rows( array $review_context ) {
136 $indexed_rows = array();
137 $groups = isset( $review_context['groups'] ) && is_array( $review_context['groups'] ) ? $review_context['groups'] : array();
138
139 foreach ( $groups as $group ) {
140 if ( ! is_array( $group ) || empty( $group['rows'] ) || ! is_array( $group['rows'] ) ) {
141 continue;
142 }
143 foreach ( $group['rows'] as $row ) {
144 if ( ! is_array( $row ) || empty( $row['step_id'] ) ) {
145 continue;
146 }
147 $indexed_rows[ sanitize_key( (string) $row['step_id'] ) ] = $row;
148 }
149 }
150
151 return $indexed_rows;
152 }
153
154 /**
155 * Return one safe Review summary with an explicit empty-state fallback.
156 *
157 * @param array<string,array<string,mixed>> $review_rows Indexed Review rows.
158 * @param string $step_id Stable step identifier.
159 *
160 * @return string Sanitized summary.
161 */
162 private function get_review_summary( array $review_rows, $step_id ) {
163 return isset( $review_rows[ $step_id ]['summary'] ) && is_scalar( $review_rows[ $step_id ]['summary'] ) && '' !== trim( (string) $review_rows[ $step_id ]['summary'] )
164 ? sanitize_text_field( (string) $review_rows[ $step_id ]['summary'] )
165 : __( 'Not configured', 'booking' );
166 }
167
168 /**
169 * Describe the canonical Standard form and the template applied to it.
170 *
171 * The selected template is a recipe applied to the existing Standard form;
172 * it is not a separately published form. Naming both identities prevents the
173 * overview from implying that the template title replaced the form name.
174 *
175 * @param array<string,array<string,mixed>> $review_rows Indexed Review rows.
176 *
177 * @return string Sanitized Booking Form summary.
178 */
179 private function get_booking_form_summary( array $review_rows ) {
180 $template_title = $this->get_review_summary( $review_rows, 'booking_form_template' );
181
182 if ( ! isset( $review_rows['booking_form_template'] ) ) {
183 return $template_title;
184 }
185
186 /* translators: 1: Booking Form name. 2: selected Setup Wizard template name. */
187 return sprintf( __( '%1$s form based on %2$s', 'booking' ), __( 'Standard', 'booking' ), $template_title );
188 }
189
190 /**
191 * Load active owner-scoped Service and assigned Provider counts.
192 *
193 * The repository loads assignments for all returned Services in one query,
194 * avoiding a Provider-assignment N+1 on the overview.
195 *
196 * @return array{services:int,providers:int} Authorized current counts.
197 */
198 private function get_service_counts() {
199 $counts = array(
200 'services' => 0,
201 'providers' => 0,
202 );
203
204 if (
205 ! function_exists( 'wpbc_appointment_services_get_manage_capability' )
206 || ! current_user_can( wpbc_appointment_services_get_manage_capability() )
207 || ! function_exists( 'wpbc_appointment_services_get_data_provider' )
208 ) {
209 return $counts;
210 }
211
212 $service_provider = wpbc_appointment_services_get_data_provider();
213 if ( ! is_object( $service_provider ) || ! method_exists( $service_provider, 'list_items_for_current_owner' ) ) {
214 return $counts;
215 }
216
217 $services = $service_provider->list_items_for_current_owner(
218 array(
219 'status' => 'active',
220 'sort_by' => 'service_id',
221 'sort_order' => 'asc',
222 'limit' => 500,
223 )
224 );
225 if ( is_wp_error( $services ) || ! is_array( $services ) ) {
226 return $counts;
227 }
228
229 $provider_ids = array();
230 $counts['services'] = count( $services );
231 foreach ( $services as $service ) {
232 $service = is_object( $service ) ? get_object_vars( $service ) : (array) $service;
233 foreach ( isset( $service['resource_ids'] ) ? (array) $service['resource_ids'] : array() as $resource_id ) {
234 $resource_id = absint( $resource_id );
235 if ( $resource_id ) {
236 $provider_ids[ $resource_id ] = true;
237 }
238 }
239 }
240 $counts['providers'] = count( $provider_ids );
241
242 return $counts;
243 }
244
245 /**
246 * Discover current public booking pages referenced by this checkpoint or by
247 * the established activation-page registry.
248 *
249 * @param array<string,mixed> $checkpoint Authorized completed checkpoint.
250 *
251 * @return array<int,array<string,mixed>> Public page cards without raw posts.
252 */
253 private function get_published_pages( array $checkpoint ) {
254 $pages = array();
255 $page_records = array();
256 $page_ids = array();
257 $result = isset( $checkpoint['step_results']['publish_integration'] ) && is_array( $checkpoint['step_results']['publish_integration'] )
258 ? $checkpoint['step_results']['publish_integration']
259 : array();
260 $publish_values = isset( $checkpoint['values']['publish_integration'] ) && is_array( $checkpoint['values']['publish_integration'] )
261 ? $checkpoint['values']['publish_integration']
262 : array();
263 $destination = isset( $publish_values['publish_destination'] ) && is_scalar( $publish_values['publish_destination'] )
264 ? sanitize_key( (string) $publish_values['publish_destination'] )
265 : '';
266 $accepted_page_types = array( 'created', 'existing' );
267 if ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_CREATE_PAGE === $destination ) {
268 $accepted_page_types = array( 'created' );
269 } elseif ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_EXISTING_PAGE === $destination ) {
270 $accepted_page_types = array( 'existing' );
271 } elseif ( '' !== $destination ) {
272 $accepted_page_types = array();
273 }
274
275 foreach ( array( 'created_ids', 'updated_ids' ) as $identifier_group ) {
276 foreach ( isset( $result[ $identifier_group ] ) ? (array) $result[ $identifier_group ] : array() as $identifier ) {
277 if (
278 is_scalar( $identifier )
279 && preg_match( '/^wp_page_(created|existing):([0-9]+)$/', (string) $identifier, $matches )
280 && in_array( $matches[1], $accepted_page_types, true )
281 ) {
282 $page_ids[ absint( $matches[2] ) ] = true;
283 }
284 }
285 }
286
287 if ( in_array( 'existing', $accepted_page_types, true ) && ! empty( $publish_values['publish_existing_page_id'] ) ) {
288 $page_ids[ absint( $publish_values['publish_existing_page_id'] ) ] = true;
289 }
290 $booking_form_values = isset( $checkpoint['values']['booking_form_template'] ) && is_array( $checkpoint['values']['booking_form_template'] )
291 ? $checkpoint['values']['booking_form_template']
292 : array();
293 $template_key = isset( $booking_form_values['booking_form_template'] ) && is_scalar( $booking_form_values['booking_form_template'] )
294 ? sanitize_key( (string) $booking_form_values['booking_form_template'] )
295 : '';
296
297 foreach ( array_keys( $page_ids ) as $page_id ) {
298 $page = get_post( $page_id );
299 if (
300 ! ( $page instanceof WP_Post )
301 || 'page' !== $page->post_type
302 || 'publish' !== $page->post_status
303 || false === strpos( (string) $page->post_content, WPBC_Setup_Wizard_Publish_Integration::MANAGED_BLOCK_START )
304 ) {
305 continue;
306 }
307 $page_url = get_permalink( $page_id );
308 if ( ! is_string( $page_url ) || '' === $page_url ) {
309 continue;
310 }
311 $page_title = sanitize_text_field( get_the_title( $page_id ) );
312 $page_records[] = array(
313 'key' => 'setup_page_' . $page_id,
314 'section' => 'setup',
315 'url' => esc_url_raw( $page_url ),
316 'page_title' => '' !== $page_title ? $page_title : __( 'Booking page', 'booking' ),
317 'button_title' => __( 'Open your booking page', 'booking' ),
318 'description' => __(
319 'Configured from the customer journey, Booking Form, appearance, and publishing choices saved in this Setup Wizard.',
320 'booking'
321 ),
322 'template_key' => $template_key,
323 );
324 }
325
326 if ( class_exists( 'WPBC_Setup_Wizard_Booking_Pages' ) ) {
327 foreach ( WPBC_Setup_Wizard_Booking_Pages::prepare_booking_page_cards( $page_records ) as $booking_page_card ) {
328 $pages[ $booking_page_card['url'] ] = $booking_page_card;
329 }
330
331 $allow_discovered_setup_page = '' === $destination || WPBC_Setup_Wizard_Publish_Integration::DESTINATION_CREATE_PAGE === $destination;
332 foreach ( WPBC_Setup_Wizard_Booking_Pages::get_booking_page_cards() as $booking_page_card ) {
333 $is_setup_page = 'setup' === ( isset( $booking_page_card['section'] ) ? $booking_page_card['section'] : '' );
334 // The completed checkpoint is authoritative; never mix in a retained page from another publishing choice.
335 if ( $is_setup_page && ( ! empty( $pages ) || ! $allow_discovered_setup_page ) ) {
336 continue;
337 }
338
339 if ( ! empty( $booking_page_card['url'] ) && ! isset( $pages[ $booking_page_card['url'] ] ) ) {
340 $pages[ $booking_page_card['url'] ] = $booking_page_card;
341 }
342 }
343 }
344
345 return array_values( $pages );
346 }
347
348 /**
349 * Create a canonical-settings link row.
350 *
351 * @param string $id Stable row ID.
352 * @param string $icon Established icon class.
353 * @param string $label Visible setting label.
354 * @param string $summary Current-choice summary.
355 * @param string $url Authorized destination URL.
356 * @param string $action_label Visible action label.
357 * @param string $summary_type Optional semantic summary style.
358 *
359 * @return array<string,mixed> Normalized row.
360 */
361 private function create_link_row( $id, $icon, $label, $summary, $url, $action_label, $summary_type = 'default' ) {
362 return array(
363 'id' => sanitize_key( $id ),
364 'icon' => sanitize_html_class( $icon ),
365 'label' => sanitize_text_field( $label ),
366 'summary' => sanitize_text_field( $summary ),
367 'summary_type' => sanitize_key( $summary_type ),
368 'action' => array(
369 'type' => 'link',
370 'label' => sanitize_text_field( $action_label ),
371 'url' => esc_url_raw( $url ),
372 ),
373 );
374 }
375
376 /**
377 * Create a nonce-protected wizard-reopen row.
378 *
379 * @param string $step_id Stable wizard step ID.
380 * @param string $icon Established icon class.
381 * @param string $label Visible setting label.
382 * @param string $summary Current-choice summary.
383 * @param string $action_label Visible action label.
384 * @param string $summary_type Optional semantic summary style.
385 *
386 * @return array<string,mixed> Normalized row.
387 */
388 private function create_wizard_row( $step_id, $icon, $label, $summary, $action_label, $summary_type = 'default' ) {
389 $row = $this->create_link_row( $step_id, $icon, $label, $summary, '', $action_label );
390 $row['action'] = array(
391 'type' => 'wizard_step',
392 'label' => sanitize_text_field( $action_label ),
393 'step_id' => sanitize_key( $step_id ),
394 );
395 $row['summary_type'] = sanitize_key( $summary_type );
396
397 return $row;
398 }
399
400 /**
401 * Create an unavailable environment-owned row without an action.
402 *
403 * @param string $id Stable row ID.
404 * @param string $icon Established icon class.
405 * @param string $label Visible setting label.
406 * @param string $summary Current-choice summary.
407 *
408 * @return array<string,mixed> Normalized row.
409 */
410 private function create_disabled_row( $id, $icon, $label, $summary ) {
411 $row = $this->create_link_row( $id, $icon, $label, $summary, '', '' );
412 $row['action']['type'] = 'none';
413 $row['summary_type'] = 'muted';
414
415 return $row;
416 }
417
418 /**
419 * Format the current active Service count.
420 *
421 * @param int $count Number of active Services.
422 *
423 * @return string Translated Service count summary.
424 */
425 private function format_service_count( $count ) {
426 /* translators: %s: Localized number of active Services. */
427 return sprintf( _x( '%s active', 'Setup overview active Service count', 'booking' ), number_format_i18n( absint( $count ) ) );
428 }
429
430 /**
431 * Format the number of Providers assigned to active Services.
432 *
433 * @param int $count Number of assigned Providers.
434 *
435 * @return string Translated Provider count summary.
436 */
437 private function format_provider_count( $count ) {
438 /* translators: %s: Localized number of assigned Providers. */
439 return sprintf( _x( '%s assigned', 'Setup overview assigned Provider count', 'booking' ), number_format_i18n( absint( $count ) ) );
440 }
441
442 /**
443 * Summarize current public-page discovery.
444 *
445 * @param array<int,array<string,mixed>> $published_pages Current public pages.
446 *
447 * @return string Publishing summary.
448 */
449 private function get_publishing_summary( array $published_pages ) {
450 $page_count = count( $published_pages );
451 if ( 0 === $page_count ) {
452 return __( 'Not published yet', 'booking' );
453 }
454
455 /* translators: %s: Localized number of published booking pages. */
456 return sprintf( _n( '%s published page', '%s published pages', $page_count, 'booking' ), number_format_i18n( $page_count ) );
457 }
458
459 /**
460 * Return a safe presentation-only notice selected by an allow-listed code.
461 *
462 * @return array<string,string> Notice type and text, or an empty array.
463 */
464 private function get_request_notice() {
465 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Presentation-only status code from an admin-post redirect.
466 $notice_code = isset( $_GET['wpbc_setup_overview_notice'] ) && is_scalar( $_GET['wpbc_setup_overview_notice'] ) ? sanitize_key( wp_unslash( $_GET['wpbc_setup_overview_notice'] ) ) : '';
467 $notices = array(
468 'stale' => array(
469 'type' => 'warning',
470 'text' => __( 'The setup state changed in another request. Reload this page and try again.', 'booking' ),
471 ),
472 'error' => array(
473 'type' => 'error',
474 'text' => __( 'The requested Setup Wizard action could not be completed. Reload this page and try again.', 'booking' ),
475 ),
476 );
477
478 return isset( $notices[ $notice_code ] ) ? $notices[ $notice_code ] : array();
479 }
480
481 /** @return string Services settings URL. */
482 private function get_services_url() {
483 return admin_url( 'admin.php?page=wpbc-services' );
484 }
485
486 /** @return string Provider settings URL. */
487 private function get_providers_url() {
488 return function_exists( 'wpbc_get_resources_url' ) ? wpbc_get_resources_url() : admin_url( 'admin.php?page=wpbc-resources' );
489 }
490
491 /** @return string Working Hours settings URL. */
492 private function get_working_hours_url() {
493 $availability_url = function_exists( 'wpbc_get_general_availability_url' ) ? wpbc_get_general_availability_url() : admin_url( 'admin.php?page=wpbc-availability&tab=general_availability' );
494
495 return add_query_arg( 'wpbc_ag_open', 'working_time', $availability_url );
496 }
497
498 /** @return string Days Off settings URL. */
499 private function get_days_off_url() {
500 return function_exists( 'wpbc_get_availability_url' ) ? wpbc_get_availability_url() : admin_url( 'admin.php?page=wpbc-availability' );
501 }
502
503 /** @return string Date Selection settings URL. */
504 private function get_date_selection_url() {
505 return function_exists( 'wpbc_settings_calendar__get_section_url' )
506 ? wpbc_settings_calendar__get_section_url( 'selection' )
507 : admin_url( 'admin.php?page=wpbc-settings&tab=calendar_settings&wpbc_calendar_section=selection' );
508 }
509
510 /** @return string Booking Form settings URL. */
511 private function get_booking_form_url() {
512 return function_exists( 'wpbc_get_settings_url' ) ? wpbc_get_settings_url() . '&tab=builder_booking_form' : admin_url( 'admin.php?page=wpbc-settings&tab=builder_booking_form' );
513 }
514
515 /** @return string Appearance settings URL. */
516 private function get_appearance_url() {
517 return function_exists( 'wpbc_get_settings_themes_url' ) ? wpbc_get_settings_themes_url() : admin_url( 'admin.php?page=wpbc-settings&tab=themes' );
518 }
519
520 /** @return string Notification settings URL. */
521 private function get_notifications_url() {
522 return function_exists( 'wpbc_get_settings_url' ) ? wpbc_get_settings_url() . '&tab=email' : admin_url( 'admin.php?page=wpbc-settings&tab=email' );
523 }
524
525 /**
526 * Return the canonical per-Resource publishing destination.
527 *
528 * The Resources domain owns its request-local Publishing view and authorized
529 * inspector launch. The fallback keeps the overview navigable if that domain
530 * helper is unavailable during a partial or compatibility load.
531 *
532 * @return string Booking Resources publishing URL.
533 */
534 private function get_publishing_url() {
535 if ( function_exists( 'wpbc_catalog_booking_resources_get_publish_url' ) ) {
536 return wpbc_catalog_booking_resources_get_publish_url();
537 }
538
539 return function_exists( 'wpbc_get_resources_url' )
540 ? wpbc_get_resources_url()
541 : admin_url( 'admin.php?page=wpbc-resources&tab=resources' );
542 }
543 }
544