PluginProbe
Booking Calendar / 11.5
Booking Calendar v11.5
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 10.11 All 204 releases
booking / includes / booking-appointment / booking-appointment__catalog.php

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

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