PluginProbe
Booking Calendar / 11.6.1
Booking Calendar v11.6.1
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 10.11 10.11.2 All 203 releases
booking / includes / page-appointment-services / appointment_services__functions.php

appointment_services__functions.php in Booking Calendar 11.6.1, at includes/page-appointment-services/appointment_services__functions.php

566 lines 24.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /** Appointment Services helpers and repository boundary. @package Booking Calendar */
3 if ( ! defined( 'ABSPATH' ) ) { exit; }
4
5 /**
6 * Determine whether the current request is the Appointment Services page.
7 *
8 * @return bool True only for the wpbc-services WordPress admin route.
9 */
10 function wpbc_appointment_services__is_page() {
11 if ( ! is_admin() ) { return false; }
12 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
13 $page = isset( $_REQUEST['page'] ) ? sanitize_key( wp_unslash( $_REQUEST['page'] ) ) : '';
14 return 'wpbc-services' === $page;
15 }
16
17 /**
18 * Return the capability required to manage Appointment Services.
19 *
20 * @return string WordPress capability name.
21 */
22 function wpbc_appointment_services_get_manage_capability() {
23 $settings_role = get_bk_option( 'booking_user_role_settings' );
24 $capabilities = array(
25 'administrator' => 'activate_plugins',
26 'editor' => 'publish_pages',
27 'author' => 'publish_posts',
28 'contributor' => 'edit_posts',
29 'subscriber' => 'read',
30 );
31 $capability = isset( $capabilities[ $settings_role ] ) ? $capabilities[ $settings_role ] : 'manage_options';
32
33 /**
34 * Filter the capability required to manage Appointment Services.
35 *
36 * @param string $capability WordPress capability name.
37 */
38 return (string) apply_filters( 'wpbc_appointment_services_manage_capability', $capability );
39 }
40
41 /**
42 * Determine whether Service pricing can use the Booking Calendar cost engine.
43 *
44 * The database fields remain available in every edition so upgrades and
45 * downgrades never require a destructive schema change. Presentation, editing,
46 * calculation, and public output use this single runtime check so editions
47 * below Business Small cannot advertise or apply an unsupported price.
48 *
49 * @return bool True when Service pricing is available in the active edition.
50 */
51 function wpbc_appointment_services_is_pricing_available() {
52 $is_available = class_exists( 'wpdev_bk_biz_s' );
53
54 /**
55 * Filter whether the active edition or an extension provides Service pricing.
56 *
57 * @param bool $is_available Whether the Business Small cost engine is loaded.
58 */
59 return (bool) apply_filters( 'wpbc_appointment_services_pricing_available', $is_available );
60 }
61
62 /**
63 * Read the requested Service inspector focus from the administration URL.
64 *
65 * The value controls only presentation after an authorized Service has loaded.
66 * Keeping an explicit allow-list prevents request data from becoming a CSS or
67 * DOM selector in the Services JavaScript client.
68 *
69 * @return string Supported inspector focus key, or an empty string.
70 */
71 function wpbc_appointment_services_get_requested_focus_section() {
72 if ( ! isset( $_GET['wpbc_service_focus'] ) || is_array( $_GET['wpbc_service_focus'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
73 return '';
74 }
75
76 $requested_focus = sanitize_key( wp_unslash( $_GET['wpbc_service_focus'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
77 $allowed_focuses = array( 'booking_form' );
78
79 return in_array( $requested_focus, $allowed_focuses, true ) ? $requested_focus : '';
80 }
81
82 /**
83 * Determine whether the current user satisfies the configured Availability role.
84 *
85 * Booking Calendar stores a minimum WordPress role in
86 * `booking_user_role_availability`. The native role helper maps that role to a
87 * hierarchical capability, allowing administrators to access pages configured
88 * for editors or lower roles. Passing the stored role directly to
89 * `current_user_can()` would incorrectly reject those higher roles.
90 *
91 * @return bool True when the current user may manage Booking Calendar availability.
92 */
93 function wpbc_appointment_services_can_manage_availability() {
94 $availability_role = sanitize_key( (string) get_bk_option( 'booking_user_role_availability' ) );
95
96 if ( '' === $availability_role ) {
97 return false;
98 }
99 if ( function_exists( 'wpbc_is_current_user_have_this_role' ) ) {
100 return wpbc_is_current_user_have_this_role( $availability_role );
101 }
102
103 return current_user_can( $availability_role );
104 }
105
106 /**
107 * Resolve the MultiUser-aware owner for Service queries and writes.
108 *
109 * @return int Owner user ID, or zero for site-wide ownership.
110 */
111 function wpbc_appointment_services_get_owner_user_id() {
112 if ( class_exists( 'WPBC_FE_Custom_Form_Helper' ) ) {
113 return absint( WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id() );
114 }
115 if ( class_exists( 'wpdev_bk_multiuser' ) ) {
116 $current_user_id = wpbc_get_current_user_id();
117 if ( ! apply_bk_filter( 'is_user_super_admin', $current_user_id ) ) { return absint( $current_user_id ); }
118 }
119 return 0;
120 }
121
122 /**
123 * Determine whether the current user may query Services from all owners.
124 *
125 * @return bool True for a Booking Calendar MultiUser super administrator.
126 */
127 function wpbc_appointment_services_can_view_all_owners() {
128 return class_exists( 'wpdev_bk_multiuser' ) && apply_bk_filter( 'is_user_super_admin', wpbc_get_current_user_id() );
129 }
130
131 /**
132 * Return existing Booking Calendar resources as Provider choices.
133 *
134 * @return array<int,string> Provider titles keyed by booking resource ID.
135 */
136 function wpbc_appointment_services_get_provider_options() {
137 $options = array();
138 $resources = apply_bk_filter( 'wpdebk_get_keyed_all_bk_resources', array() );
139 foreach ( (array) $resources as $resource_id => $resource ) {
140 $resource = is_object( $resource ) ? get_object_vars( $resource ) : (array) $resource;
141 $id = ! empty( $resource['id'] ) ? absint( $resource['id'] ) : absint( $resource_id );
142 if ( $id ) { $options[ $id ] = ! empty( $resource['title'] ) ? wpbc_lang( $resource['title'] ) : sprintf( __( 'Provider #%d', 'booking' ), $id ); }
143 }
144 if ( empty( $options ) && ! class_exists( 'wpdev_bk_personal' ) ) {
145 $title = function_exists( 'wpbc_get_resource_title' ) ? wpbc_get_resource_title( 1 ) : '';
146 $options[1] = $title ? $title : __( 'Default Provider', 'booking' );
147 }
148 return apply_filters( 'wpbc_appointment_service_provider_options', $options );
149 }
150
151 /**
152 * Build the Provider-specific Working Time administration URL.
153 *
154 * The Services catalog exposes only a recurring weekday summary. This URL
155 * opens the native General Availability page at the Provider-aware Working
156 * Time section where that summary can be edited.
157 *
158 * @param int $resource_id Provider booking resource ID.
159 *
160 * @return string Provider availability URL, or an empty string when the
161 * current user cannot manage availability.
162 */
163 function wpbc_appointment_services_get_provider_availability_url( $resource_id ) {
164 $resource_id = absint( $resource_id );
165
166 if ( ! $resource_id || ! wpbc_appointment_services_can_manage_availability() ) {
167 return '';
168 }
169
170 $availability_url = function_exists( 'wpbc_get_general_availability_url' )
171 ? wpbc_get_general_availability_url()
172 : admin_url( 'admin.php?page=wpbc-availability&tab=general_availability' );
173
174 return esc_url_raw(
175 add_query_arg(
176 array(
177 'resource_id' => $resource_id,
178 'wpbc_ag_open' => 'working_time',
179 ),
180 $availability_url
181 )
182 );
183 }
184
185 /**
186 * Resolve the recurring weekday availability known for one Provider.
187 *
188 * General unavailable weekdays are authoritative. When Working Time is
189 * enabled, weekdays without any effective interval are also unavailable.
190 * Date-specific and seasonal exceptions remain in the native calendar engine
191 * and are intentionally not guessed by this lightweight administration state.
192 *
193 * @param int $resource_id Provider booking resource ID.
194 * @param array<string,mixed> $resource Optional source resource values.
195 *
196 * @return array<string,bool> Availability keyed by mon through sun.
197 */
198 function wpbc_appointment_services_get_provider_weekday_availability( $resource_id, $resource = array() ) {
199 $weekdays = array(
200 'mon' => 'On' !== get_bk_option( 'booking_unavailable_day1' ),
201 'tue' => 'On' !== get_bk_option( 'booking_unavailable_day2' ),
202 'wed' => 'On' !== get_bk_option( 'booking_unavailable_day3' ),
203 'thu' => 'On' !== get_bk_option( 'booking_unavailable_day4' ),
204 'fri' => 'On' !== get_bk_option( 'booking_unavailable_day5' ),
205 'sat' => 'On' !== get_bk_option( 'booking_unavailable_day6' ),
206 'sun' => 'On' !== get_bk_option( 'booking_unavailable_day0' ),
207 );
208
209 if ( function_exists( 'wpbc_working_time__get_effective_rule' ) ) {
210 $working_time_rule = wpbc_working_time__get_effective_rule( absint( $resource_id ) );
211 if ( is_array( $working_time_rule ) && isset( $working_time_rule['weekdays'] ) ) {
212 $day_numbers = array(
213 'mon' => 1,
214 'tue' => 2,
215 'wed' => 3,
216 'thu' => 4,
217 'fri' => 5,
218 'sat' => 6,
219 'sun' => 0,
220 );
221 foreach ( $day_numbers as $day_key => $day_number ) {
222 $weekdays[ $day_key ] = $weekdays[ $day_key ] && ! empty( $working_time_rule['weekdays'][ $day_number ] );
223 }
224 }
225 }
226
227 return (array) apply_filters( 'wpbc_appointment_service_provider_weekday_availability', $weekdays, absint( $resource_id ), $resource );
228 }
229
230 /**
231 * Determine whether a Provider has at least one recurring bookable weekday.
232 *
233 * @param int $resource_id Provider booking resource ID.
234 * @param array<string,mixed> $resource Optional source resource values.
235 *
236 * @return bool True when one or more weekdays are available.
237 */
238 function wpbc_appointment_services_provider_has_weekly_availability( $resource_id, $resource = array() ) {
239 return in_array( true, wpbc_appointment_services_get_provider_weekday_availability( $resource_id, $resource ), true );
240 }
241
242 /**
243 * Resolve the public image configured for one Provider Booking Resource.
244 *
245 * Existing Business Large Searchable Resources values remain the fast path
246 * because callers already load that option collection once for a Provider
247 * list. When that legacy field is not configured, the resolver uses the
248 * cross-edition Booking Resource content repository so Free's bundled default
249 * image and pictures saved by the Resource catalog reach every Provider UI.
250 * The guarded fallback keeps this presentation helper safe during partial
251 * bootstrap and compatibility requests where the content repository has not
252 * been loaded yet.
253 *
254 * @param int $provider_id Provider Booking Resource ID.
255 * @param array<int,array<string,mixed>> $search_options Searchable Resource options keyed by resource ID.
256 *
257 * @return string Sanitized Provider image URL or an empty string.
258 */
259 function wpbc_appointment_services_get_provider_image_url( $provider_id, $search_options ) {
260 $provider_id = absint( $provider_id );
261 if ( ! $provider_id ) {
262 return '';
263 }
264
265 if ( ! empty( $search_options[ $provider_id ]['picture'] ) ) {
266 $image_value = $search_options[ $provider_id ]['picture'];
267 if ( is_array( $image_value ) ) {
268 $image_value = reset( $image_value );
269 }
270 if ( ! is_scalar( $image_value ) ) {
271 return '';
272 }
273
274 return esc_url_raw( wpbc_lang( (string) $image_value ) );
275 }
276
277 if ( ! function_exists( 'wpbc_booking_resource_content_repository' ) ) {
278 return '';
279 }
280
281 // Booking Calendar Free has exactly one implicit Booking Resource (ID 1).
282 if ( ! class_exists( 'wpdev_bk_personal' ) && 1 !== $provider_id ) {
283 return '';
284 }
285
286 $resource_content = wpbc_booking_resource_content_repository()->get( $provider_id );
287 if ( empty( $resource_content['picture_url'] ) || ! is_scalar( $resource_content['picture_url'] ) ) {
288 return '';
289 }
290
291 return esc_url_raw( wpbc_lang( (string) $resource_content['picture_url'] ) );
292 }
293
294 /**
295 * Provider presentation data for the Services management table.
296 *
297 * Booking resources remain the availability authority. The default weekday
298 * map combines General Availability with each resource's effective Working
299 * Time rule. Cross-edition Booking Resource pictures are reused before a
300 * WordPress user avatar, keeping the administration catalog consistent with
301 * the public Appointment flow. Extensions may replace the presentation or
302 * recurring-weekday summary through the filters below without changing the
303 * Services page contract.
304 *
305 * @return array<int,array<string,mixed>>
306 */
307 function wpbc_appointment_services_get_provider_directory() {
308 $directory = array();
309 $options = wpbc_appointment_services_get_provider_options();
310 $resources = apply_bk_filter( 'wpdebk_get_keyed_all_bk_resources', array() );
311 $resources_by_id = array();
312 $search_options = function_exists( 'wpbc_searchable_resources__get_all_options' )
313 ? (array) wpbc_searchable_resources__get_all_options()
314 : array();
315
316 foreach ( (array) $resources as $resource_id => $resource ) {
317 $resource = is_object( $resource ) ? get_object_vars( $resource ) : (array) $resource;
318 $id = ! empty( $resource['id'] ) ? absint( $resource['id'] ) : absint( $resource_id );
319 if ( $id ) { $resources_by_id[ $id ] = $resource; }
320 }
321
322 foreach ( $options as $resource_id => $title ) {
323 $resource = isset( $resources_by_id[ $resource_id ] ) ? $resources_by_id[ $resource_id ] : array();
324 $user_id = ! empty( $resource['users'] ) ? absint( $resource['users'] ) : 0;
325 $resource_image_url = wpbc_appointment_services_get_provider_image_url( $resource_id, $search_options );
326 $wp_user_avatar_url = $user_id ? get_avatar_url( $user_id, array( 'size' => 64 ) ) : '';
327 $words = preg_split( '/\s+/u', trim( wp_strip_all_tags( $title ) ) );
328 $initials = '';
329 foreach ( array_slice( array_filter( (array) $words ), 0, 2 ) as $word ) {
330 $initials .= function_exists( 'mb_substr' ) ? mb_substr( $word, 0, 1 ) : substr( $word, 0, 1 );
331 }
332 $provider_weekdays = wpbc_appointment_services_get_provider_weekday_availability( $resource_id, $resource );
333 $entry = array(
334 'id' => absint( $resource_id ),
335 'title' => $title,
336 'initials' => strtoupper( $initials ? $initials : 'P' ),
337 'avatar_url' => $resource_image_url ? $resource_image_url : $wp_user_avatar_url,
338 'availability_url' => wpbc_appointment_services_get_provider_availability_url( $resource_id ),
339 'weekdays' => $provider_weekdays,
340 'has_weekly_availability' => in_array( true, $provider_weekdays, true ),
341 );
342 $directory[ $resource_id ] = apply_filters( 'wpbc_appointment_service_provider_presentation', $entry, absint( $resource_id ), $resource );
343 }
344
345 return $directory;
346 }
347
348 /**
349 * Return published Booking Forms that can be assigned to a Service.
350 *
351 * @return array<int,string> Form titles keyed by form ID; zero means the default form.
352 */
353 function wpbc_appointment_services_get_form_options() {
354 $options = array( 0 => __( 'Default Booking Form', 'booking' ) );
355 if ( class_exists( 'WPBC_FE_Custom_Form_Helper' ) ) {
356 $forms = WPBC_FE_Custom_Form_Helper::get_custom_booking_forms_list(
357 array( 'include_standard' => false, 'owner_user_id' => wpbc_appointment_services_get_owner_user_id(), 'statuses' => array( 'published' ) )
358 );
359 foreach ( $forms as $form ) {
360 if ( ! empty( $form['id'] ) ) { $options[ absint( $form['id'] ) ] = ! empty( $form['title'] ) ? $form['title'] : $form['name']; }
361 }
362 }
363 return apply_filters( 'wpbc_appointment_service_form_options', $options );
364 }
365
366 /**
367 * Return the configured Appointment Services data provider.
368 *
369 * The native repository supplies list_items(), count_items(), find(), save(),
370 * duplicate(), and archive(). Extensions may replace it through the provider
371 * filter; count_items() enables efficient server-side catalog pagination.
372 *
373 * @return object|null
374 */
375 function wpbc_appointment_services_get_data_provider() {
376 return apply_filters( 'wpbc_appointment_services_data_provider', null );
377 }
378
379 /**
380 * Determine whether the configured Service storage is ready for requests.
381 *
382 * @return bool True when a provider exists and reports usable storage.
383 */
384 function wpbc_appointment_services_storage_is_ready() {
385 $provider = wpbc_appointment_services_get_data_provider();
386 if ( ! is_object( $provider ) || ! method_exists( $provider, 'list_items' ) ) { return false; }
387 return method_exists( $provider, 'is_ready' ) ? (bool) $provider->is_ready() : true;
388 }
389
390 /**
391 * Build the standard error returned when Service storage is unavailable.
392 *
393 * @return WP_Error Storage-not-ready error with administrator guidance.
394 */
395 function wpbc_appointment_services_storage_error() {
396 return new WP_Error( 'appointment_services_storage_unavailable', __( 'The Services database is not ready. Reload the page as an administrator or reactivate Booking Calendar to finish the database upgrade.', 'booking' ) );
397 }
398
399 /**
400 * Sanitize and normalize a Service payload at the repository boundary.
401 *
402 * @param mixed $payload Raw Service values from AJAX or another provider.
403 *
404 * @return array<string,mixed> Safe values with defaults and bounded numeric fields.
405 */
406 function wpbc_appointment_services_sanitize_payload( $payload ) {
407 $payload = is_array( $payload ) ? $payload : array();
408 $title = isset( $payload['title'] ) ? sanitize_text_field( $payload['title'] ) : '';
409 $title = wp_html_excerpt( $title, 200, '' );
410 $metadata = wpbc_appointment_services_decode_metadata( isset( $payload['metadata'] ) ? $payload['metadata'] : array() );
411 $picture_url = array_key_exists( 'picture_url', $payload ) ? $payload['picture_url'] : ( isset( $metadata['picture_url'] ) ? $metadata['picture_url'] : '' );
412 $picture_url = is_scalar( $picture_url ) ? esc_url_raw( trim( (string) $picture_url ) ) : '';
413 return array(
414 'service_id' => isset( $payload['service_id'] ) ? absint( $payload['service_id'] ) : 0,
415 'title' => $title,
416 'description' => isset( $payload['description'] ) ? sanitize_textarea_field( $payload['description'] ) : '',
417 'picture_url' => $picture_url,
418 'status' => isset( $payload['status'] ) && in_array( $payload['status'], array( 'active', 'inactive', 'archived' ), true ) ? $payload['status'] : 'active',
419 'duration_minutes' => isset( $payload['duration_minutes'] ) ? min( 65535, max( 1, absint( $payload['duration_minutes'] ) ) ) : 30,
420 'buffer_before_minutes' => isset( $payload['buffer_before_minutes'] ) ? min( 65535, absint( $payload['buffer_before_minutes'] ) ) : 0,
421 'buffer_after_minutes' => isset( $payload['buffer_after_minutes'] ) ? min( 65535, absint( $payload['buffer_after_minutes'] ) ) : 0,
422 'base_cost' => isset( $payload['base_cost'] ) && is_numeric( $payload['base_cost'] ) ? number_format( min( 9999999999.99, max( 0, (float) $payload['base_cost'] ) ), 2, '.', '' ) : '0.00',
423 'booking_form_id' => isset( $payload['booking_form_id'] ) ? absint( $payload['booking_form_id'] ) : 0,
424 'resource_ids' => isset( $payload['resource_ids'] ) ? array_values( array_filter( array_map( 'absint', (array) $payload['resource_ids'] ) ) ) : array(),
425 );
426 }
427
428 /**
429 * Apply nullable Provider-assignment overrides to one Service row.
430 *
431 * The returned `duration_minutes` and `base_cost` are effective values used by
432 * the Appointment flow. Original Service values and the applied override are
433 * retained separately so snapshots and extensions can explain their source.
434 *
435 * @param mixed $service Service row optionally containing assignment overrides.
436 *
437 * @return array<string,mixed> Service row with effective scheduling values.
438 */
439 function wpbc_appointment_services_apply_assignment_overrides( $service ) {
440 $service = is_object( $service ) ? get_object_vars( $service ) : (array) $service;
441
442 $base_duration_minutes = isset( $service['duration_minutes'] ) ? min( 65535, absint( $service['duration_minutes'] ) ) : 0;
443 $has_duration_override = array_key_exists( 'duration_override', $service ) && null !== $service['duration_override'] && absint( $service['duration_override'] ) > 0;
444 $duration_override_minutes = $has_duration_override ? min( 65535, absint( $service['duration_override'] ) ) : 0;
445 $base_service_cost = isset( $service['base_cost'] ) && is_numeric( $service['base_cost'] ) ? min( 9999999999.99, max( 0, (float) $service['base_cost'] ) ) : 0.0;
446 $has_cost_override = array_key_exists( 'cost_override', $service ) && null !== $service['cost_override'] && is_numeric( $service['cost_override'] );
447 $cost_override = $has_cost_override ? min( 9999999999.99, max( 0, (float) $service['cost_override'] ) ) : null;
448
449 $service['base_duration_minutes'] = $base_duration_minutes;
450 $service['duration_override_minutes'] = $duration_override_minutes;
451 $service['duration_minutes'] = $has_duration_override ? $duration_override_minutes : $base_duration_minutes;
452 $service['base_service_cost'] = number_format( $base_service_cost, 2, '.', '' );
453 $service['cost_override'] = null === $cost_override ? null : number_format( $cost_override, 2, '.', '' );
454 $service['base_cost'] = number_format( null === $cost_override ? $base_service_cost : $cost_override, 2, '.', '' );
455
456 return (array) apply_filters( 'wpbc_appointment_service_effective_assignment', $service );
457 }
458
459 /**
460 * Return a stable public title for a Provider resource.
461 *
462 * @param int $resource_id Booking resource acting as the Provider.
463 *
464 * @return string Localized plain-text Provider title.
465 */
466 function wpbc_appointment_services_get_provider_title( $resource_id ) {
467 $resource_id = absint( $resource_id );
468 $title = function_exists( 'wpbc_get_resource_title' ) ? wpbc_get_resource_title( $resource_id ) : '';
469 if ( '' === trim( wp_strip_all_tags( (string) $title ) ) ) {
470 $options = wpbc_appointment_services_get_provider_options();
471 $title = isset( $options[ $resource_id ] ) ? $options[ $resource_id ] : sprintf( __( 'Provider #%d', 'booking' ), $resource_id );
472 }
473
474 return sanitize_text_field( wp_strip_all_tags( wpbc_lang( (string) $title ) ) );
475 }
476
477 /**
478 * Decode an Appointment Service metadata value into an associative array.
479 *
480 * @param mixed $metadata JSON string or already-decoded metadata.
481 *
482 * @return array<string,mixed> Decoded metadata, or an empty array for invalid input.
483 */
484 function wpbc_appointment_services_decode_metadata( $metadata ) {
485 if ( is_array( $metadata ) ) {
486 return $metadata;
487 }
488
489 $decoded = json_decode( (string) $metadata, true );
490
491 return is_array( $decoded ) ? $decoded : array();
492 }
493
494 /**
495 * Decode Appointment snapshot metadata using the shared metadata parser.
496 *
497 * This compatibility wrapper preserves the existing snapshot helper while
498 * allowing Service records and snapshots to share one defensive JSON parser.
499 *
500 * @param mixed $metadata JSON string or already-decoded metadata.
501 *
502 * @return array<string,mixed> Decoded metadata, or an empty array for invalid input.
503 */
504 function wpbc_appointment_services_decode_snapshot_metadata( $metadata ) {
505 return wpbc_appointment_services_decode_metadata( $metadata );
506 }
507
508 /**
509 * Normalize a provider result into the stable Service response contract.
510 *
511 * @param mixed $service Service row returned by a data provider.
512 *
513 * @return array<string,mixed> Normalized Service item.
514 */
515 function wpbc_appointment_services_normalize_item( $service ) {
516 $service = is_object( $service ) ? get_object_vars( $service ) : (array) $service;
517 return wp_parse_args( wpbc_appointment_services_sanitize_payload( $service ), array( 'service_id' => 0, 'title' => '' ) );
518 }
519
520 /**
521 * Normalize Service status counts returned by a repository.
522 *
523 * @param mixed $raw_counts Repository count response.
524 *
525 * @return array<string,int> Counts keyed by all, active, inactive, and archived.
526 */
527 function wpbc_appointment_services_normalize_status_counts( $raw_counts ) {
528 $raw_counts = is_array( $raw_counts ) ? $raw_counts : array();
529 $counts = array(
530 'all' => 0,
531 'active' => isset( $raw_counts['active'] ) ? absint( $raw_counts['active'] ) : 0,
532 'inactive' => isset( $raw_counts['inactive'] ) ? absint( $raw_counts['inactive'] ) : 0,
533 'archived' => isset( $raw_counts['archived'] ) ? absint( $raw_counts['archived'] ) : 0,
534 );
535 $counts['all'] = $counts['active'] + $counts['inactive'] + $counts['archived'];
536
537 return $counts;
538 }
539
540 /**
541 * Authorize an Appointment Services AJAX request.
542 *
543 * @return void Terminates with JSON when the nonce or capability check fails.
544 */
545 function wpbc_appointment_services_ajax_authorize() {
546 if ( false === check_ajax_referer( 'wpbc_appointment_services_ajax_nonce', 'nonce', false ) ) {
547 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'booking' ) ), 403 );
548 }
549 if ( ! current_user_can( wpbc_appointment_services_get_manage_capability() ) ) {
550 wp_send_json_error( array( 'message' => __( 'You do not have permission to manage services.', 'booking' ) ), 403 );
551 }
552 }
553
554 /**
555 * Send a consistent JSON error for a failed data-provider operation.
556 *
557 * @param mixed $result Provider result, optionally a WP_Error instance.
558 * @param string $fallback Fallback message when no provider error is available.
559 *
560 * @return void Terminates with a JSON error response.
561 */
562 function wpbc_appointment_services_send_provider_error( $result, $fallback ) {
563 $message = is_wp_error( $result ) ? $result->get_error_message() : $fallback;
564 wp_send_json_error( array( 'message' => $message ), 500 );
565 }
566