PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
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 / booking-appointment / booking-appointment__catalog.php

booking-appointment__catalog.php in Booking Calendar 11.7, at includes/booking-appointment/booking-appointment__catalog.php

292 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Public Service and Provider catalogue for Appointment booking.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Resolve the public image configured for one Provider Booking Resource.
14 *
15 * The shared Services resolver supports legacy Searchable Resources pictures
16 * and the cross-edition Booking Resource content repository. Initials remain
17 * the renderer fallback only when neither source provides a safe image URL.
18 *
19 * @param int $provider_id Provider Booking Resource ID.
20 * @param array<int,array<string,mixed>> $search_options Searchable Resource options keyed by resource ID.
21 *
22 * @return string Sanitized Provider image URL or an empty string.
23 */
24 function wpbc_booking_appointment_get_provider_image_url( $provider_id, $search_options ) {
25 return wpbc_appointment_services_get_provider_image_url( $provider_id, $search_options );
26 }
27
28 /**
29 * Load active Service assignment rows for the public Provider set.
30 *
31 * The native repository uses one joined query. Replacement repositories retain
32 * compatibility through their existing list_active_for_resource() contract.
33 *
34 * @param int[] $provider_ids Provider resource IDs.
35 * @param object $repository Configured Service repository.
36 *
37 * @return array<int,array<string,mixed>> Service rows containing resource_id.
38 */
39 function wpbc_booking_appointment_get_assignment_rows( $provider_ids, $repository ) {
40 global $wpdb;
41
42 $provider_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $provider_ids ) ) ) );
43 if ( empty( $provider_ids ) ) {
44 return array();
45 }
46
47 if ( $repository instanceof WPBC_Appointment_Services_Repository ) {
48 $placeholders = implode( ',', array_fill( 0, count( $provider_ids ), '%d' ) );
49 $sql = 'SELECT s.*, sr.resource_id, sr.duration_override, sr.cost_override, sr.priority AS assignment_priority
50 FROM ' . wpbc_appointment_services_table_name( 'services' ) . ' s
51 INNER JOIN ' . wpbc_appointment_services_table_name( 'service_resources' ) . ' sr ON sr.service_id = s.service_id
52 WHERE s.status = %s AND sr.status = %s AND sr.resource_id IN (' . $placeholders . ')
53 ORDER BY s.title, s.service_id, sr.priority, sr.assignment_id';
54 $args = array_merge( array( 'active', 'active' ), $provider_ids );
55
56 return (array) $wpdb->get_results( $wpdb->prepare( $sql, $args ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
57 }
58
59 $rows = array();
60 foreach ( $provider_ids as $provider_id ) {
61 foreach ( (array) $repository->list_active_for_resource( $provider_id ) as $service ) {
62 $service = is_object( $service ) ? get_object_vars( $service ) : (array) $service;
63 $service['resource_id'] = $provider_id;
64 $rows[] = $service;
65 }
66 }
67
68 return $rows;
69 }
70
71 /**
72 * Order public Services for the Appointment selection step.
73 *
74 * An explicit shortcode Service list is both a restriction and a presentation
75 * sequence. When no sequence is supplied, the existing natural title order is
76 * retained. Any unexpected Service not present in the requested sequence is
77 * appended alphabetically so filters and replacement repositories do not lose
78 * catalogue entries.
79 *
80 * @param array<int,array<string,mixed>> $services Services keyed by Service ID.
81 * @param int[] $requested_service_ids Requested Service IDs in display order.
82 *
83 * @return array<int,array<string,mixed>> Ordered Services keyed by Service ID.
84 */
85 function wpbc_booking_appointment_order_services( $services, $requested_service_ids = array() ) {
86 $services = (array) $services;
87 $requested_service_ids = wpbc_booking_appointment_normalize_ids( $requested_service_ids );
88 $title_sort_callback = static function ( $left, $right ) {
89 return strnatcasecmp( (string) $left['title'], (string) $right['title'] );
90 };
91
92 if ( empty( $requested_service_ids ) ) {
93 uasort( $services, $title_sort_callback );
94 return $services;
95 }
96
97 $ordered_services = array();
98 foreach ( $requested_service_ids as $service_id ) {
99 if ( isset( $services[ $service_id ] ) ) {
100 $ordered_services[ $service_id ] = $services[ $service_id ];
101 }
102 }
103
104 $remaining_services = array_diff_key( $services, $ordered_services );
105 uasort( $remaining_services, $title_sort_callback );
106
107 return $ordered_services + $remaining_services;
108 }
109
110 /**
111 * Build the public Service catalogue from active resource assignments.
112 *
113 * This intentionally uses the existing repository contract rather than an
114 * owner-scoped administrator query. A Service is public only when it is active
115 * and actively assigned to a Provider resource available in this context.
116 *
117 * @param array<string,mixed> $config Normalized shortcode configuration.
118 *
119 * @return array{services:array<int,array<string,mixed>>,providers:array<int,array<string,mixed>>,diagnostics:array<string,int>}|WP_Error Catalogue or storage error.
120 */
121 function wpbc_booking_appointment_get_catalog( $config ) {
122 if ( ! function_exists( 'wpbc_appointment_services_repository' ) || ! wpbc_appointment_services_storage_is_ready() ) {
123 return new WP_Error( 'appointment_storage_unavailable', __( 'Appointment Services are not available.', 'booking' ) );
124 }
125
126 $config = wpbc_booking_appointment_normalize_config( $config );
127 $repository = wpbc_appointment_services_repository();
128 $provider_options = wpbc_appointment_services_get_provider_options();
129 $provider_options = (array) apply_filters( 'wpbc_booking_appointment_public_provider_options', $provider_options, $config );
130 $search_options = array();
131 if ( function_exists( 'wpbc_searchable_resources__get_all_options' ) ) {
132 $search_options = (array) wpbc_searchable_resources__get_all_options();
133 }
134
135 if ( $config['provider_ids'] ) {
136 $provider_options = array_intersect_key( $provider_options, array_fill_keys( $config['provider_ids'], true ) );
137 }
138
139 $services = array();
140 $providers = array();
141 $pricing_available = wpbc_appointment_services_is_pricing_available();
142 $maximum_duration = absint( apply_filters( 'wpbc_booking_appointment_maximum_duration_minutes', 24 * 60, $config ) );
143 $assignment_rows = wpbc_booking_appointment_get_assignment_rows( array_keys( $provider_options ), $repository );
144 foreach ( $assignment_rows as $service ) {
145 $provider_id = ! empty( $service['resource_id'] ) ? absint( $service['resource_id'] ) : 0;
146 if ( ! $provider_id || ! isset( $provider_options[ $provider_id ] ) ) {
147 continue;
148 }
149
150 $base_service = wpbc_appointment_services_normalize_item( $service );
151 $effective_service = wpbc_appointment_services_apply_assignment_overrides( $service );
152 if ( ! $pricing_available ) {
153 $base_service['base_cost'] = '';
154 }
155 $service_id = absint( $base_service['service_id'] );
156 $duration_minutes = absint( $effective_service['duration_minutes'] );
157 if ( ! $service_id || ! $duration_minutes || ( $maximum_duration && $duration_minutes > $maximum_duration ) || ( $config['service_ids'] && ! in_array( $service_id, $config['service_ids'], true ) ) ) {
158 continue;
159 }
160
161 if ( ! isset( $services[ $service_id ] ) ) {
162 $base_service['resource_ids'] = array();
163 $base_service['provider_rules'] = array();
164 $services[ $service_id ] = $base_service;
165 }
166 $services[ $service_id ]['resource_ids'][] = $provider_id;
167 $services[ $service_id ]['provider_rules'][ $provider_id ] = array(
168 'duration_minutes' => absint( $effective_service['duration_minutes'] ),
169 'base_duration_minutes' => absint( $effective_service['base_duration_minutes'] ),
170 'duration_override_minutes' => absint( $effective_service['duration_override_minutes'] ),
171 'base_cost' => $pricing_available ? (string) $effective_service['base_cost'] : '',
172 'base_service_cost' => $pricing_available ? (string) $effective_service['base_service_cost'] : '',
173 'cost_override' => $pricing_available ? $effective_service['cost_override'] : null,
174 );
175
176 if ( ! isset( $providers[ $provider_id ] ) ) {
177 $providers[ $provider_id ] = array(
178 'provider_id' => $provider_id,
179 'title' => wp_strip_all_tags( wpbc_lang( $provider_options[ $provider_id ] ) ),
180 'image_url' => wpbc_booking_appointment_get_provider_image_url( $provider_id, $search_options ),
181 'service_ids' => array(),
182 'has_weekly_availability' => function_exists( 'wpbc_appointment_services_provider_has_weekly_availability' )
183 ? wpbc_appointment_services_provider_has_weekly_availability( $provider_id )
184 : true,
185 );
186 }
187 $providers[ $provider_id ]['service_ids'][] = $service_id;
188 }
189
190 foreach ( $services as &$service ) {
191 $service['resource_ids'] = array_values( array_unique( array_map( 'absint', $service['resource_ids'] ) ) );
192 }
193 unset( $service );
194 foreach ( $providers as &$provider ) {
195 $provider['service_ids'] = array_values( array_unique( array_map( 'absint', $provider['service_ids'] ) ) );
196 }
197 unset( $provider );
198
199 $services = wpbc_booking_appointment_order_services( $services, $config['service_ids'] );
200
201 $catalog = array(
202 'services' => $services,
203 'providers' => $providers,
204 'diagnostics' => array(
205 'provider_count' => count( $provider_options ),
206 'assignment_count' => count( $assignment_rows ),
207 ),
208 );
209
210 return (array) apply_filters( 'wpbc_booking_appointment_public_catalog', $catalog, $config );
211 }
212
213 /**
214 * Return one selectable Service from the public catalogue.
215 *
216 * @param array<string,mixed> $catalog Public catalogue.
217 * @param int $service_id Requested Service ID.
218 *
219 * @return array<string,mixed>|WP_Error Service or public validation error.
220 */
221 function wpbc_booking_appointment_get_service( $catalog, $service_id ) {
222 $service_id = absint( $service_id );
223 if ( ! $service_id || empty( $catalog['services'][ $service_id ] ) ) {
224 return new WP_Error( 'appointment_service_unavailable', __( 'The selected Service is not available. Please choose another Service.', 'booking' ) );
225 }
226
227 return $catalog['services'][ $service_id ];
228 }
229
230 /**
231 * Return Providers compatible with one public Service.
232 *
233 * @param array<string,mixed> $catalog Public catalogue.
234 * @param array<string,mixed> $service Selected Service.
235 *
236 * @return array<int,array<string,mixed>> Compatible Providers keyed by resource ID.
237 */
238 function wpbc_booking_appointment_get_service_providers( $catalog, $service ) {
239 $providers = array();
240 foreach ( (array) $service['resource_ids'] as $provider_id ) {
241 $provider_id = absint( $provider_id );
242 if ( isset( $catalog['providers'][ $provider_id ] ) ) {
243 $providers[ $provider_id ] = $catalog['providers'][ $provider_id ];
244 }
245 }
246
247 return (array) apply_filters( 'wpbc_booking_appointment_service_providers', $providers, $service, $catalog );
248 }
249
250 /**
251 * Return one compatible Provider from a public Service catalogue.
252 *
253 * @param array<string,mixed> $catalog Public catalogue.
254 * @param array<string,mixed> $service Selected Service.
255 * @param int $provider_id Requested Provider resource ID.
256 *
257 * @return array<string,mixed>|WP_Error Provider or compatibility error.
258 */
259 function wpbc_booking_appointment_get_provider( $catalog, $service, $provider_id ) {
260 $providers = wpbc_booking_appointment_get_service_providers( $catalog, $service );
261 $provider_id = absint( $provider_id );
262 if ( ! $provider_id || empty( $providers[ $provider_id ] ) ) {
263 return new WP_Error( 'appointment_provider_unavailable', __( 'The selected Provider is not available for this Service. Please choose another Provider.', 'booking' ) );
264 }
265
266 return $providers[ $provider_id ];
267 }
268
269 /**
270 * Resolve the Service values effective for one selected Provider.
271 *
272 * The Service picker continues to show the base Service duration. Once a
273 * Provider is selected, its nullable assignment overrides become authoritative
274 * for native form rendering and match the server-side save lookup.
275 *
276 * @param array<string,mixed> $service Public Service definition.
277 * @param int $provider_id Selected Provider resource ID.
278 *
279 * @return array<string,mixed> Service definition with effective values.
280 */
281 function wpbc_booking_appointment_get_effective_service( $service, $provider_id ) {
282 $provider_id = absint( $provider_id );
283 $rules = ! empty( $service['provider_rules'][ $provider_id ] ) && is_array( $service['provider_rules'][ $provider_id ] )
284 ? $service['provider_rules'][ $provider_id ]
285 : array();
286 if ( $rules ) {
287 $service = array_merge( $service, $rules );
288 }
289
290 return (array) apply_filters( 'wpbc_booking_appointment_effective_service', $service, $provider_id );
291 }
292