PluginProbe ʕ •ᴥ•ʔ
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress / 5.6.2
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress v5.6.2
5.6.8 5.6.7 5.6.6 5.6.5 5.6.4 5.6.3 5.6.2 5.6.1 5.6.0 5.5.2 5.5.1 5.5.0 5.4.2 trunk 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.1.91 5.1.92 5.1.93 5.1.94 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1
latepoint / lib / helpers / booking_helper.php
latepoint / lib / helpers Last commit date
activities_helper.php 4 months ago agent_helper.php 4 months ago analytics_helper.php 2 months ago auth_helper.php 2 months ago blocks_helper.php 4 weeks ago booking_helper.php 4 weeks ago bricks_helper.php 4 months ago bundles_helper.php 3 months ago calendar_helper.php 4 weeks ago carts_helper.php 4 months ago connector_helper.php 4 months ago csv_helper.php 4 months ago customer_helper.php 2 months ago customer_import_helper.php 1 month ago database_helper.php 4 months ago debug_helper.php 4 months ago defaults_helper.php 4 months ago elementor_helper.php 4 months ago email_helper.php 4 months ago encrypt_helper.php 4 months ago events_helper.php 3 months ago form_helper.php 4 months ago icalendar_helper.php 4 months ago image_helper.php 4 months ago invoices_helper.php 4 weeks ago license_helper.php 4 months ago location_helper.php 4 months ago marketing_systems_helper.php 4 months ago meeting_systems_helper.php 4 months ago menu_helper.php 4 weeks ago meta_helper.php 4 months ago migrations_helper.php 4 months ago money_helper.php 3 months ago notifications_helper.php 4 months ago nps_survey_helper.php 4 months ago order_intent_helper.php 2 months ago orders_helper.php 3 months ago otp_helper.php 3 months ago pages_helper.php 4 months ago params_helper.php 4 months ago payments_helper.php 2 months ago plugin_version_update_helper.php 2 months ago price_breakdown_helper.php 4 months ago process_jobs_helper.php 4 months ago processes_helper.php 4 months ago razorpay_connect_helper.php 1 month ago replacer_helper.php 4 months ago resource_helper.php 4 months ago roles_helper.php 4 weeks ago router_helper.php 4 months ago service_helper.php 4 months ago sessions_helper.php 4 months ago settings_helper.php 4 weeks ago short_links_systems_helper.php 4 months ago shortcodes_helper.php 4 weeks ago sms_helper.php 4 months ago steps_helper.php 4 weeks ago stripe_connect_helper.php 2 months ago styles_helper.php 4 months ago support_topics_helper.php 4 months ago time_helper.php 3 months ago timeline_helper.php 2 months ago transaction_helper.php 1 month ago transaction_intent_helper.php 4 months ago util_helper.php 1 month ago version_specific_updates_helper.php 4 months ago whatsapp_helper.php 1 month ago work_periods_helper.php 3 months ago wp_datetime.php 4 months ago wp_user_helper.php 4 months ago
booking_helper.php
2284 lines
1 <?php
2
3 class OsBookingHelper {
4
5
6 /**
7 * @param OsBookingModel $booking
8 *
9 * @return mixed|void
10 *
11 * Returns full amount to charge in database format 1999.0000
12 *
13 */
14 public static function calculate_full_amount_for_booking( OsBookingModel $booking ) {
15 if ( ! $booking->service_id ) {
16 return 0;
17 }
18 $amount = self::calculate_full_amount_for_service( $booking );
19 $amount = apply_filters( 'latepoint_calculate_full_amount_for_booking', $amount, $booking );
20 $amount = OsMoneyHelper::pad_to_db_format( $amount );
21
22 return $amount;
23 }
24
25
26 /**
27 * @param OsBookingModel $booking
28 *
29 * @return mixed|void
30 *
31 */
32 public static function calculate_full_amount_for_service( OsBookingModel $booking ) {
33 if ( ! $booking->service_id ) {
34 return 0;
35 }
36 $service = new OsServiceModel( $booking->service_id );
37 $amount_for_service = $service->get_full_amount_for_duration( $booking->duration );
38 $amount_for_service = apply_filters( 'latepoint_full_amount_for_service', $amount_for_service, $booking );
39
40 return $amount_for_service;
41 }
42
43
44 /**
45 * @param OsBookingModel $booking
46 *
47 * @return mixed|void
48 *
49 * Returns deposit amount to charge in database format 1999.0000
50 *
51 */
52 public static function calculate_deposit_amount_to_charge( OsBookingModel $booking ) {
53 if ( ! $booking->service_id ) {
54 return 0;
55 }
56 $service = new OsServiceModel( $booking->service_id );
57 $amount = $service->get_deposit_amount_for_duration( $booking->duration );
58 $amount = apply_filters( 'latepoint_deposit_amount_for_service', $amount, $booking );
59 $amount = OsMoneyHelper::pad_to_db_format( $amount );
60
61 return $amount;
62 }
63
64
65 /**
66 * @param array $item_data
67 *
68 * @return OsBookingModel
69 */
70 public static function build_booking_model_from_item_data( array $item_data ): OsBookingModel {
71 $booking = new OsBookingModel();
72 if ( $item_data['id'] ) {
73 $booking = $booking->load_by_id( $item_data['id'] );
74 if ( ! $booking ) {
75 $booking = new OsBookingModel();
76 }
77 }
78 $booking->set_data( $item_data );
79 // get buffers from service and set to booking object
80 if ( ! isset( $item_data['buffer_before'] ) && ! isset( $item_data['buffer_after'] ) ) {
81 $booking->set_buffers();
82 }
83 if ( empty( $booking->end_time ) ) {
84 $booking->calculate_end_date_and_time();
85 }
86 if ( empty( $booking->end_date ) ) {
87 $booking->calculate_end_date();
88 }
89 $booking->set_utc_datetimes();
90
91 return $booking;
92 }
93
94 public static function get_booking_id_and_manage_ability_by_key( string $key ) {
95 $booking_id = OsMetaHelper::get_booking_id_by_meta_value( 'key_to_manage_for_agent', $key );
96 if ( $booking_id ) {
97 return [
98 'booking_id' => $booking_id,
99 'for' => 'agent',
100 ];
101 }
102
103 $booking_id = OsMetaHelper::get_booking_id_by_meta_value( 'key_to_manage_for_customer', $key );
104 if ( $booking_id ) {
105 return [
106 'booking_id' => $booking_id,
107 'for' => 'customer',
108 ];
109 }
110
111 return false;
112 }
113
114 public static function is_action_allowed( string $action, OsBookingModel $booking, string $key = '' ) {
115 $is_allowed = false;
116 if ( empty( $booking->id ) ) {
117 return false;
118 }
119 if ( ! in_array( $action, [ 'cancel', 'reschedule' ] ) ) {
120 return false;
121 }
122 $action_result = false;
123 switch ( $action ) {
124 case 'cancel':
125 $action_result = OsCustomerHelper::can_cancel_booking( $booking );
126 break;
127 case 'reschedule':
128 $action_result = OsCustomerHelper::can_reschedule_booking( $booking );
129 break;
130 }
131 if ( ! empty( $key ) ) {
132 // key is passed, check if allowed through key
133 $agent_key_meta = OsMetaHelper::get_booking_meta_by_key( 'key_to_manage_for_agent', $booking->id );
134 $customer_key_meta = OsMetaHelper::get_booking_meta_by_key( 'key_to_manage_for_customer', $booking->id );
135 if ( $key == $agent_key_meta ) {
136 // agent can do everything, no need to check for action
137 $is_allowed = true;
138 } elseif ( $key == $customer_key_meta ) {
139 // customer
140 $is_allowed = $action_result;
141 }
142 } elseif ( OsAuthHelper::get_logged_in_customer_id() == $booking->customer_id ) {
143 $is_allowed = $action_result;
144 }
145
146 return $is_allowed;
147 }
148
149 public static function generate_add_to_calendar_links( OsBookingModel $booking, $key = false ): string {
150 $html = '<div class="add-to-calendar-types">
151 <div class="atc-heading-wrapper">
152 <div class="atc-heading">' . esc_html__( 'Calendar Type', 'latepoint' ) . '</div>
153 <div class="close-calendar-types"></div>
154 </div>
155 <a href="' . esc_url( $booking->get_ical_download_link( $key ) ) . '" target="_blank" class="atc-type atc-type-apple">
156 <div class="atc-type-image"></div>
157 <div class="atc-type-name">' . esc_html__( 'Apple Calendar', 'latepoint' ) . '</div>
158 </a>
159 <a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'google' ) ) . '" target="_blank" class="atc-type atc-type-google">
160 <div class="atc-type-image"></div>
161 <div class="atc-type-name">' . esc_html__( 'Google Calendar', 'latepoint' ) . '</div>
162 </a>
163 <a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'outlook' ) ) . '" target="_blank" class="atc-type atc-type-outlook">
164 <div class="atc-type-image"></div>
165 <div class="atc-type-name">' . esc_html__( 'Outlook.com', 'latepoint' ) . '</div>
166 </a>
167 <a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'outlook' ) ) . '" target="_blank" class="atc-type atc-type-office-365">
168 <div class="atc-type-image"></div>
169 <div class="atc-type-name">' . esc_html__( 'Microsoft 365', 'latepoint' ) . '</div>
170 </a>
171 </div>';
172
173 return $html;
174 }
175
176
177 public static function get_bookings_for_select( $should_be_in_future = false ) {
178 $bookings = new OsBookingModel();
179 if ( $should_be_in_future ) {
180 $bookings = $bookings->should_be_in_future();
181 }
182 $bookings = $bookings->order_by( 'id desc' )->set_limit( 100 )->get_results_as_models();
183 $bookings_options = [];
184 foreach ( $bookings as $booking ) {
185 $name = $booking->service->name . ', ' . $booking->agent->full_name . ', ' . $booking->customer->full_name . ' [' . $booking->booking_code . ' : ' . $booking->id . ']';
186 $bookings_options[] = [
187 'value' => $booking->id,
188 'label' => esc_html( $name ),
189 ];
190 }
191
192 return $bookings_options;
193 }
194
195 /**
196 *
197 * Determine whether to show
198 *
199 * @param $rows
200 *
201 * @return bool
202 */
203 public static function is_breakdown_free( $rows ) {
204 return ( ( empty( $rows['subtotal']['raw_value'] ) || ( (float) $rows['subtotal']['raw_value'] <= 0 ) ) && ( empty( $rows['total']['raw_value'] ) || ( (float) $rows['total']['raw_value'] <= 0 ) ) );
205 }
206
207 public static function output_price_breakdown( $rows ) {
208 foreach ( $rows['before_subtotal'] as $row ) {
209 self::output_price_breakdown_row( $row );
210 }
211 // if there is nothing between subtotal and total - don't show subtotal as it will be identical to total
212 if ( ! empty( $rows['after_subtotal'] ) ) {
213 if ( ! empty( $rows['subtotal'] ) ) {
214 echo '<div class="subtotal-separator"></div>';
215 self::output_price_breakdown_row( $rows['subtotal'] );
216 }
217 foreach ( $rows['after_subtotal'] as $row ) {
218 self::output_price_breakdown_row( $row );
219 }
220 }
221 if ( ! empty( $rows['total'] ) ) {
222 self::output_price_breakdown_row( $rows['total'] );
223 }
224 if ( ! empty( $rows['payments'] ) ) {
225 foreach ( $rows['payments'] as $row ) {
226 self::output_price_breakdown_row( $row );
227 }
228 }
229 if ( ! empty( $rows['balance'] ) ) {
230 self::output_price_breakdown_row( $rows['balance'] );
231 }
232 }
233
234 public static function output_price_breakdown_row( $row ) {
235 if ( ! empty( $row['items'] ) ) {
236 if ( ! empty( $row['heading'] ) ) {
237 echo '<div class="summary-box-heading"><div class="sbh-item">' . esc_html( $row['heading'] ) . '</div><div class="sbh-line"></div></div>';
238 }
239 foreach ( $row['items'] as $row_item ) {
240 self::output_price_breakdown_row( $row_item );
241 }
242 } else {
243 $extra_class = '';
244 if ( isset( $row['style'] ) && $row['style'] == 'strong' ) {
245 $extra_class .= ' spi-strong';
246 }
247 if ( isset( $row['style'] ) && $row['style'] == 'total' ) {
248 $extra_class .= ' spi-total';
249 }
250 if ( isset( $row['type'] ) && $row['type'] == 'credit' ) {
251 $extra_class .= ' spi-positive';
252 }
253 ?>
254 <div class="summary-price-item-w <?php echo esc_attr( $extra_class ); ?>">
255 <div class="spi-name">
256 <?php echo $row['label']; ?>
257 <?php if ( ! empty( $row['note'] ) ) {
258 echo '<span class="pi-note">' . esc_html( $row['note'] ) . '</span>';
259 } ?>
260 <?php if ( ! empty( $row['badge'] ) ) {
261 echo '<span class="pi-badge">' . esc_html( $row['badge'] ) . '</span>';
262 } ?>
263 </div>
264 <div class="spi-price"><?php echo esc_html( $row['value'] ); ?></div>
265 </div>
266 <?php
267 }
268 }
269
270 public static function output_price_breakdown_row_as_input_field( $row, $base_name ) {
271 $field_name = $base_name . '[' . OsUtilHelper::random_text( 'alnum', 8 ) . ']';
272 if ( ! empty( $row['items'] ) ) {
273 echo OsFormHelper::hidden_field( $field_name . '[heading]', $row['heading'] ?? '' );
274 foreach ( $row['items'] as $row_item ) {
275 self::output_price_breakdown_row_as_input_field( $row_item, $field_name . '[items]' );
276 }
277 } else {
278 $is_negative = ( $row['raw_value'] < 0 );
279 $wrapper_class = $is_negative ? [ 'class' => 'green-value-input' ] : [];
280 $label = $row['label'] ?? '';
281 if ( ! empty( $row['note'] ) ) {
282 $label .= ' ' . $row['note'];
283 }
284 // Use abs() for money_field because inputmask cannot parse negative initial values; the sign is preserved via the 'type' hidden field
285 $field_value = $is_negative ? abs( $row['raw_value'] ) : $row['raw_value'];
286 echo OsFormHelper::money_field( $field_name . '[value]', $label, $field_value, [ 'theme' => 'right-aligned' ], [], $wrapper_class );
287 echo OsFormHelper::hidden_field( $field_name . '[label]', $row['label'] ?? '' );
288 echo OsFormHelper::hidden_field( $field_name . '[style]', $row['style'] ?? '' );
289 echo OsFormHelper::hidden_field( $field_name . '[type]', $row['type'] ?? '' );
290 echo OsFormHelper::hidden_field( $field_name . '[note]', $row['note'] ?? '' );
291 echo OsFormHelper::hidden_field( $field_name . '[badge]', $row['badge'] ?? '' );
292 }
293 if ( ! empty( $row['sub_items'] ) ) {
294 foreach ( $row['sub_items'] as $row_item ) {
295 self::output_price_breakdown_row_as_input_field( $row_item, $field_name . '[sub_items]' );
296 }
297 }
298 }
299
300 /**
301 * @param \LatePoint\Misc\Filter $filter
302 * @param bool $accessed_from_backend
303 *
304 * @return array
305 */
306 public static function get_blocked_periods_grouped_by_day( \LatePoint\Misc\Filter $filter, bool $accessed_from_backend = false ): array {
307 $grouped_blocked_periods = [];
308
309 if ( $filter->date_from ) {
310 $date_from = OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
311 $date_to = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
312
313 # Loop through days to fill in days that might have no bookings
314 for ( $day = clone $date_from; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
315 $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] = [];
316 }
317 }
318 if ( ! $accessed_from_backend ) {
319 $today = new OsWpDateTime( 'today' );
320 $earliest_possible_booking = OsSettingsHelper::get_earliest_possible_booking_restriction( $filter->service_id );
321
322 $block_end_datetime = OsTimeHelper::now_datetime_object();
323 if ( $earliest_possible_booking ) {
324 try {
325 $block_end_datetime->modify( $earliest_possible_booking );
326 } catch ( Exception $e ) {
327 $block_end_datetime = OsTimeHelper::now_datetime_object();
328 }
329 }
330 for ( $day = clone $today; $day->format( 'Y-m-d' ) <= $block_end_datetime->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
331 // loop days from now to the earliest possible booking and block timeslots if these days were actually requested
332 if ( isset( $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] ) ) {
333 $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ][] = new \LatePoint\Misc\BlockedPeriod(
334 [
335 'start_time' => 0,
336 'end_time' => ( $day->format( 'Y-m-d' ) < $block_end_datetime->format( 'Y-m-d' ) ) ? 24 * 60 : OsTimeHelper::convert_datetime_to_minutes( $block_end_datetime ),
337 'start_date' => $day->format( 'Y-m-d' ),
338 'end_date' => $day->format( 'Y-m-d' ),
339 ]
340 );
341 }
342 }
343 $latest_possible_booking = OsSettingsHelper::get_latest_possible_booking_restriction( $filter->service_id );
344 if ( $latest_possible_booking ) {
345 try {
346 $latest_booking_datetime = OsTimeHelper::now_datetime_object();
347 $latest_booking_datetime->modify( $latest_possible_booking );
348 } catch ( Exception $e ) {
349 $latest_booking_datetime = null;
350 }
351 if ( $latest_booking_datetime && $filter->date_from ) {
352 $date_to = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
353 // Start from the latest_booking_datetime day
354 for ( $day = clone $latest_booking_datetime; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
355 if ( isset( $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] ) ) {
356 $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ][] = new \LatePoint\Misc\BlockedPeriod(
357 [
358 'start_time' => ( $day->format( 'Y-m-d' ) == $latest_booking_datetime->format( 'Y-m-d' ) ) ? OsTimeHelper::convert_datetime_to_minutes( $latest_booking_datetime ) : 0,
359 'end_time' => 24 * 60,
360 'start_date' => $day->format( 'Y-m-d' ),
361 'end_date' => $day->format( 'Y-m-d' ),
362 ]
363 );
364 }
365 }
366 }
367 }
368 }
369
370 $grouped_blocked_periods = apply_filters( 'latepoint_blocked_periods_for_range', $grouped_blocked_periods, $filter );
371
372 return $grouped_blocked_periods;
373 }
374
375 /**
376 * @param \LatePoint\Misc\Filter $filter
377 *
378 * @return array
379 */
380 public static function get_booked_periods_grouped_by_day( \LatePoint\Misc\Filter $filter ): array {
381 $booked_periods = self::get_booked_periods( $filter );
382
383 $grouped_booked_periods = [];
384 if ( $filter->date_from ) {
385 $date_from = OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
386 $date_to = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
387
388 # Loop through days to fill in days that might have no bookings
389 for ( $day = clone $date_from; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
390 $grouped_booked_periods[ $day->format( 'Y-m-d' ) ] = [];
391 }
392 foreach ( $booked_periods as $booked_period ) {
393 $grouped_booked_periods[ $booked_period->start_date ][] = $booked_period;
394 // if event spans multiple days - add to other days as well
395 if ( $booked_period->end_date && ( $booked_period->start_date != $booked_period->end_date ) ) {
396 $grouped_booked_periods[ $booked_period->end_date ][] = $booked_period;
397 }
398 }
399 }
400
401 return $grouped_booked_periods;
402 }
403
404 /**
405 * @param \LatePoint\Misc\Filter $filter
406 *
407 * @return \LatePoint\Misc\BookedPeriod[]
408 */
409 public static function get_booked_periods( \LatePoint\Misc\Filter $filter ): array {
410
411
412 $bookings = self::get_bookings( $filter, true );
413 $booked_periods = [];
414
415 foreach ( $bookings as $booking ) {
416 $booked_periods[] = \LatePoint\Misc\BookedPeriod::create_from_booking_model( $booking );
417 }
418
419
420 if ( $filter->consider_cart_items ) {
421 $cart = OsCartsHelper::get_or_create_cart();
422 $bookings_in_cart = $cart->get_bookings_from_cart_items();
423
424 foreach ( $bookings_in_cart as $cart_booking ) {
425 $booked_periods[] = \LatePoint\Misc\BookedPeriod::create_from_booking_model( $cart_booking );
426 }
427 }
428
429 // TODO Update all filters to accept new "filter" variable (In Google Calendar addon)
430 $booked_periods = apply_filters( 'latepoint_get_booked_periods', $booked_periods, $filter );
431
432 return $booked_periods;
433 }
434
435
436 /**
437 * @param \LatePoint\Misc\Filter $filter
438 * @param bool $as_models
439 *
440 * @return array
441 */
442 public static function get_bookings( \LatePoint\Misc\Filter $filter, bool $as_models = false ): array {
443 $bookings = new OsBookingModel();
444 if ( $filter->date_from ) {
445 if ( $filter->date_from && $filter->date_to ) {
446 # both start and end date provided - means it's a range
447 $bookings->where(
448 [
449 'start_date >=' => $filter->date_from,
450 'start_date <=' => $filter->date_to,
451 ]
452 );
453 } else {
454 # only start_date provided - means it's a specific date requested
455 $bookings->where( [ 'start_date' => $filter->date_from ] );
456 }
457 }
458
459
460 if ( $filter->connections ) {
461 $connection_conditions = [];
462 foreach ( $filter->connections as $connection ) {
463 $connection_conditions[] = [
464 'AND' =>
465 [
466 'agent_id' => $connection->agent_id,
467 'service_id' => $connection->service_id,
468 'location_id' => $connection->location_id,
469 ],
470 ];
471 }
472 $bookings->where( [ 'OR' => $connection_conditions ] );
473 } else {
474 if ( $filter->agent_id ) {
475 $bookings->where( [ 'agent_id' => $filter->agent_id ] );
476 }
477 if ( $filter->location_id ) {
478 $bookings->where( [ 'location_id' => $filter->location_id ] );
479 }
480 if ( $filter->service_id ) {
481 $bookings->where( [ 'service_id' => $filter->service_id ] );
482 }
483 }
484 if ( $filter->statuses ) {
485 $bookings->where( [ 'status' => $filter->statuses ] );
486 }
487 if ( $filter->exclude_booking_ids ) {
488 $bookings->where( [ 'id NOT IN' => $filter->exclude_booking_ids ] );
489 }
490 $bookings->order_by( 'start_time asc, end_time asc, service_id asc' );
491 $bookings = ( $as_models ) ? $bookings->get_results_as_models() : $bookings->get_results();
492
493 // make sure to return empty array if nothing is found
494 if ( empty( $bookings ) ) {
495 $bookings = [];
496 }
497
498 return $bookings;
499 }
500
501 public static function generate_ical_event_string( $booking ) {
502 // translators: %1$s is agent name, %2$s is service name
503 $booking_description = sprintf( __( 'Appointment with %1$s for %2$s', 'latepoint' ), $booking->agent->full_name, $booking->service->name );
504
505 $ics = new ICS(
506 array(
507 'location' => $booking->location->full_address,
508 'description' => '',
509 'dtstart' => $booking->format_start_date_and_time_for_google(),
510 'dtend' => $booking->format_end_date_and_time_for_google(),
511 'summary' => $booking_description,
512 'url' => get_site_url(),
513 )
514 );
515
516 return $ics->to_string();
517 }
518
519 /**
520 * @param \LatePoint\Misc\BookingRequest $booking_request
521 *
522 * @return bool
523 *
524 * Checks if requested booking slot is available, loads work periods and booked periods from database and checks availability against them
525 */
526 public static function is_booking_request_available( \LatePoint\Misc\BookingRequest $booking_request, $settings = [] ): bool {
527 try {
528 $requested_date = new OsWpDateTime( $booking_request->start_date );
529 } catch ( Exception $e ) {
530 return false;
531 }
532 $resources = OsResourceHelper::get_resources_grouped_by_day( $booking_request, $requested_date, $requested_date, $settings );
533 if ( empty( $resources[ $requested_date->format( 'Y-m-d' ) ] ) ) {
534 return false;
535 }
536 $is_available = false;
537
538
539 // check if satisfies earliest and latest bookings - check per-service settings first, then global
540 $earliest_possible_booking = OsSettingsHelper::get_earliest_possible_booking_restriction( $booking_request->service_id );
541 $latest_possible_booking = OsSettingsHelper::get_latest_possible_booking_restriction( $booking_request->service_id );
542
543
544 if ( $earliest_possible_booking || $latest_possible_booking ) {
545 // check earliest
546 if ( ! empty( $earliest_possible_booking ) ) {
547 try {
548 $earliest_possible_booking_date = new OsWpDateTime( $earliest_possible_booking );
549 if ( $earliest_possible_booking_date > $booking_request->get_start_datetime() ) {
550 return false;
551 }
552 } catch ( Exception $e ) {
553
554 }
555 }
556 if ( ! empty( $latest_possible_booking ) ) {
557 // check latest
558 try {
559 $latest_possible_booking_date = new OsWpDateTime( $latest_possible_booking );
560 if ( $latest_possible_booking_date < $booking_request->get_start_datetime() ) {
561 return false;
562 }
563 } catch ( Exception $e ) {
564
565 }
566 }
567 }
568
569 foreach ( $resources[ $requested_date->format( 'Y-m-d' ) ] as $resource ) {
570 foreach ( $resource->slots as $slot ) {
571 if ( $slot->start_time == $booking_request->start_time && $slot->can_accomodate( $booking_request->total_attendees ) ) {
572 $is_available = true;
573 }
574 if ( $is_available ) {
575 break;
576 }
577 }
578 if ( $is_available ) {
579 break;
580 }
581 }
582
583 return $is_available;
584 }
585
586 /**
587 *
588 * Checks if two bookings are part of the same group appointment
589 *
590 * @param bool|OsBookingModel $booking
591 * @param bool|OsBookingModel $compare_booking
592 *
593 * @return bool
594 */
595 public static function check_if_group_bookings( $booking, $compare_booking ): bool {
596 if ( $booking && $compare_booking && ( $compare_booking->start_time == $booking->start_time ) && ( $compare_booking->end_time == $booking->end_time ) && ( $compare_booking->service_id == $booking->service_id ) && ( $compare_booking->location_id == $booking->location_id ) ) {
597 return true;
598 } else {
599 return false;
600 }
601 }
602
603
604 public static function process_actions_after_save( $booking_id ) {
605 }
606
607 /**
608 * @param DateTime $start_date
609 * @param DateTime $end_date
610 * @param \LatePoint\Misc\BookingRequest $booking_request
611 * @param \LatePoint\Misc\BookingResource[] $resources
612 * @param array $settings
613 *
614 * @return string
615 * @throws Exception
616 */
617 public static function get_quick_availability_days( DateTime $start_date, DateTime $end_date, \LatePoint\Misc\BookingRequest $booking_request, array $resources = [], array $settings = [] ) {
618 $default_settings = [
619 'work_boundaries' => false,
620 'exclude_booking_ids' => [],
621 ];
622 $settings = array_merge( $default_settings, $settings );
623
624 $html = '';
625
626 if ( ! $resources ) {
627 $resources = OsResourceHelper::get_resources_grouped_by_day( $booking_request, $start_date, $end_date, $settings );
628 }
629 if ( ! $settings['work_boundaries'] ) {
630 $settings['work_boundaries'] = OsResourceHelper::get_work_boundaries_for_groups_of_resources( $resources );
631 }
632
633 if ( $start_date->format( 'j' ) != '1' ) {
634 $html .= '<div class="ma-month-label">' . OsUtilHelper::get_month_name_by_number( $start_date->format( 'n' ) ) . '</div>';
635 }
636
637 for ( $day_date = clone $start_date; $day_date <= $end_date; $day_date->modify( '+1 day' ) ) {
638 // first day of month, output month name
639 if ( $day_date->format( 'j' ) == '1' ) {
640 $html .= '<div class="ma-month-label">' . OsUtilHelper::get_month_name_by_number( $day_date->format( 'n' ) ) . '</div>';
641 }
642 $html .= '<div class="ma-day ma-day-number-' . $day_date->format( 'N' ) . '">';
643 $html .= '<div class="ma-day-info">';
644 $html .= '<span class="ma-day-number">' . $day_date->format( 'j' ) . '</span>';
645 $html .= '<span class="ma-day-weekday">' . OsUtilHelper::get_weekday_name_by_number( $day_date->format( 'N' ), true ) . '</span>';
646 $html .= '</div>';
647 $html .= OsTimelineHelper::availability_timeline( $booking_request, $settings['work_boundaries'], $resources[ $day_date->format( 'Y-m-d' ) ], [ 'book_on_click' => false ] );
648 $html .= '</div>';
649 }
650
651 return $html;
652 }
653
654 public static function count_pending_bookings() {
655 $bookings = new OsBookingModel();
656
657 return $bookings->filter_allowed_records()->where( [ 'status IN' => OsBookingHelper::get_booking_statuses_for_pending_page() ] )->count();
658 }
659
660
661 public static function generate_bundles_folder( ?array $bundles = null, array $restrictions = [] ): void {
662 if ( null === $bundles ) {
663 $bundles_model = new OsBundleModel();
664 $bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
665 }
666
667 if ( $bundles ) {
668 /**
669 * Whether to skip the "Bundle & Save" folder header and render bundle
670 * tiles directly (without a collapsible category wrapper).
671 *
672 * @param bool $skip Whether to skip the folder wrapper. Default false.
673 * @param array $restrictions Active booking-form restrictions.
674 *
675 * @hook latepoint_bundles_folder_skip_wrapper
676 */
677 $skip_folder_wrapper = (bool) apply_filters( 'latepoint_bundles_folder_skip_wrapper', false, $restrictions );
678
679 if ( ! $skip_folder_wrapper ) {
680 ?>
681 <div class="os-item-category-w os-items os-as-rows os-animated-child">
682 <div class="os-item-category-info-w os-item os-animated-self with-plus">
683 <div class="os-item-category-info os-item-i" tabindex="0">
684 <div class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></div>
685 <div class="os-item-name-w">
686 <div class="os-item-name"><?php echo esc_html__( 'Bundle & Save', 'latepoint' ); ?></div>
687 </div>
688 <?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $bundles ) ) { ?>
689 <div class="os-item-child-count">
690 <span><?php echo count( $bundles ); ?></span> <?php esc_html_e( 'Bundles', 'latepoint' ); ?>
691 </div>
692 <?php } ?>
693 </div>
694 </div>
695 <?php
696 }
697 ?>
698 <div class="os-bundles os-animated-parent os-items os-as-rows os-selectable-items">
699 <?php
700 foreach ( $bundles as $bundle ) { ?>
701 <div class="os-animated-child os-item os-selectable-item <?php echo ( $bundle->charge_amount ) ? 'os-priced-item' : ''; ?> <?php if ( $bundle->short_description ) {
702 echo 'with-description'; } ?>"
703 tabindex="0"
704 data-item-price="<?php echo esc_attr( $bundle->charge_amount ); ?>"
705 data-priced-item-type="bundle"
706 data-summary-field-name="bundle"
707 data-summary-value="<?php echo esc_attr( $bundle->name ); ?>"
708 data-item-id="<?php echo esc_attr( $bundle->id ); ?>"
709 data-cart-item-item-data-key="bundle_id"
710 data-os-call-func="latepoint_bundle_selected">
711 <div class="os-service-selector os-item-i os-animated-self"
712 data-bundle-id="<?php echo esc_attr( $bundle->id ); ?>">
713 <span class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></span>
714 <span class="os-item-name-w">
715 <span class="os-item-name"><?php echo esc_html( $bundle->name ); ?></span>
716 <?php if ( $bundle->short_description ) { ?>
717 <span class="os-item-desc"><?php echo wp_kses_post( $bundle->short_description ); ?></span>
718 <?php } ?>
719 </span>
720
721 <?php if ( $bundle->charge_amount > 0 ) { ?>
722 <span class="os-item-price-w">
723 <span class="os-item-price">
724 <?php echo esc_html( OsMoneyHelper::format_price( $bundle->charge_amount ) ); ?>
725 </span>
726 </span>
727 <?php } ?>
728 </div>
729 </div>
730 <?php
731 }
732 ?>
733 </div>
734 <?php
735 if ( ! $skip_folder_wrapper ) {
736 echo '</div>';
737 }
738 }
739 }
740
741 public static function generate_services_list( $services = false, $preselected_service = false ) {
742 if ( $services && is_array( $services ) && ! empty( $services ) ) { ?>
743 <div class="os-services os-animated-parent os-items os-as-rows os-selectable-items">
744 <?php foreach ( $services as $service ) {
745 // if service is preselected - only output that service, skip the rest
746 if ( $preselected_service && $service->id != $preselected_service->id ) {
747 continue;
748 }
749 $service_durations = $service->get_all_durations_arr();
750 $is_priced = ( ! ( count( $service_durations ) > 1 ) && $service->charge_amount ) ? true : false;
751 ?>
752 <div class="os-animated-child os-item os-selectable-item <?php echo ( $preselected_service && $service->id == $preselected_service->id ) ? 'selected is-preselected' : ''; ?> <?php echo ( $is_priced ) ? 'os-priced-item' : ''; ?> <?php if ( $service->short_description ) {
753 echo 'with-description'; } ?>"
754 tabindex="0"
755 data-item-price="<?php echo esc_attr( $service->charge_amount ); ?>"
756 data-priced-item-type="service"
757 data-summary-field-name="service"
758 data-summary-value="<?php echo esc_attr( $service->name ); ?>"
759 data-item-id="<?php echo esc_attr( $service->id ); ?>"
760 data-cart-item-item-data-key="service_id"
761 data-os-call-func="latepoint_service_selected"
762 data-id-holder=".latepoint_service_id">
763 <div class="os-service-selector os-item-i os-animated-self" data-service-id="<?php echo esc_attr( $service->id ); ?>">
764 <?php if ( $service->selection_image_id ) { ?>
765 <span class="os-item-img-w" style="background-image: url(<?php echo esc_url( $service->selection_image_url ); ?>);"></span>
766 <?php } ?>
767 <span class="os-item-name-w">
768 <span class="os-item-name"><?php echo esc_html( $service->name ); ?></span>
769 <?php if ( $service->short_description ) { ?>
770 <span class="os-item-desc"><?php echo wp_kses_post( $service->short_description ); ?></span>
771 <?php } ?>
772 </span>
773 <?php if ( $service->price_min > 0 ) { ?>
774 <span class="os-item-price-w">
775 <span class="os-item-price">
776 <?php
777 /**
778 * Filters the display price value shown on the service tile on a booking form
779 *
780 * @since 5.1.94
781 * @hook latepoint_booking_form_display_service_price
782 *
783 * @param {string} $price displayed price that will be outputted
784 * @param {OsServiceModel} $service Service that the price is displayed for
785 *
786 * @returns {string} Filtered displayed price
787 */
788 $display_price = apply_filters( 'latepoint_booking_form_display_service_price', $service->price_min_formatted, $service );
789 echo esc_html( $display_price ) ?>
790 </span>
791 <?php if ( $service->price_min != $service->price_max ) { ?>
792 <span class="os-item-price-label"><?php esc_html_e( 'Starts From', 'latepoint' ); ?></span>
793 <?php } ?>
794 </span>
795 <?php } ?>
796 </div>
797 </div>
798 <?php } ?>
799 </div>
800 <?php }
801 }
802
803 public static function generate_services_bundles_and_categories_list( $parent_id = false, array $settings = [] ) {
804 $default_settings = [
805 'show_service_categories_arr' => false,
806 'show_services_arr' => false,
807 'preselected_service' => false,
808 'preselected_category' => false,
809 'bundles' => null,
810 'service_display_mode' => 'all',
811 ];
812 $settings = array_merge( $default_settings, $settings );
813
814 if ( $settings['preselected_service'] ) {
815 OsBookingHelper::generate_services_list( [ $settings['preselected_service'] ], $settings['preselected_service'] );
816
817 return;
818 }
819
820 $service_categories = new OsServiceCategoryModel();
821 $args = array();
822 if ( $settings['show_service_categories_arr'] && is_array( $settings['show_service_categories_arr'] ) ) {
823 if ( $parent_id ) {
824 $service_categories->where( [ 'parent_id' => $parent_id ] );
825 } else {
826 if ( $settings['preselected_category'] ) {
827 $service_categories->where( [ 'id' => $settings['preselected_category'] ] );
828 } else {
829 $service_categories->where_in( 'id', $settings['show_service_categories_arr'] );
830 $service_categories->where(
831 [
832 'parent_id' => [
833 'OR' => [
834 'IS NULL',
835 ' NOT IN' => $settings['show_service_categories_arr'],
836 ],
837 ],
838 ]
839 );
840 }
841 }
842 } else {
843 if ( $settings['preselected_category'] ) {
844 $service_categories->where( [ 'id' => $settings['preselected_category'] ] );
845 } else {
846 $args['parent_id'] = $parent_id ? $parent_id : 'IS NULL';
847 }
848 }
849 $service_categories = $service_categories->where( $args )->order_by( 'order_number asc' )->get_results_as_models();
850
851 $main_parent_class = ( $parent_id ) ? 'os-animated-parent' : 'os-item-categories-main-parent os-animated-parent';
852 if ( ! $settings['preselected_category'] ) {
853 echo '<div class="os-item-categories-holder ' . esc_attr( $main_parent_class ) . '">';
854 }
855
856 // generate services that have no category
857 if ( $parent_id == false && $settings['preselected_category'] == false ) { ?>
858 <?php
859 $services_without_category = new OsServiceModel();
860 if ( $settings['show_services_arr'] ) {
861 $services_without_category->where_in( 'id', $settings['show_services_arr'] );
862 }
863 $services_without_category = $services_without_category->where( [ 'category_id' => 0 ] )->should_be_active()->should_not_be_hidden()->get_results_as_models();
864 /** This filter is documented in lib/helpers/steps_helper.php */
865 $services_without_category = apply_filters( 'latepoint_step_services', $services_without_category, $settings['bundles'], [ 'service_display_mode' => $settings['service_display_mode'] ] );
866 if ( $services_without_category ) {
867 OsBookingHelper::generate_services_list( $services_without_category, false );
868 }
869 }
870
871 if ( is_array( $service_categories ) ) {
872 foreach ( $service_categories as $service_category ) { ?>
873 <?php
874 $services = [];
875 $category_services = $service_category->get_active_services();
876 if ( is_array( $category_services ) ) {
877 // if show selected services restriction is set - filter
878 if ( $settings['show_services_arr'] ) {
879 foreach ( $category_services as $category_service ) {
880 if ( in_array( $category_service->id, $settings['show_services_arr'] ) ) {
881 $services[] = $category_service;
882 }
883 }
884 } else {
885 $services = $category_services;
886 }
887 }
888 /** This filter is documented in lib/helpers/steps_helper.php */
889 $services = apply_filters( 'latepoint_step_services', $services, $settings['bundles'], [ 'service_display_mode' => $settings['service_display_mode'] ] );
890 $child_categories = new OsServiceCategoryModel();
891 $count_child_categories = $child_categories->where( [ 'parent_id' => $service_category->id ] )->count();
892 // show only if it has either at least one child category or service
893 if ( $count_child_categories || count( $services ) ) {
894 // preselected category, just show contents, not the wrapper
895 if ( $service_category->id == $settings['preselected_category'] ) {
896 OsBookingHelper::generate_services_list( $services, false );
897 OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) );
898 } else { ?>
899 <div class="os-item-category-w os-items os-as-rows os-animated-child"
900 data-id="<?php echo esc_attr( $service_category->id ); ?>">
901 <div class="os-item-category-info-w os-item os-animated-self with-plus">
902 <div class="os-item-category-info os-item-i">
903 <div class="os-item-img-w"
904 style="background-image: url(<?php echo esc_url( $service_category->selection_image_url ); ?>);"></div>
905 <div class="os-item-name-w">
906 <div class="os-item-name"><?php echo esc_html( $service_category->name ); ?></div>
907 <?php if ( ! empty( $service_category->short_description ) ) { ?>
908 <div class="os-item-desc"><?php echo $service_category->short_description; ?></div>
909 <?php } ?>
910 </div>
911 <?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $services ) ) { ?>
912 <div class="os-item-child-count">
913 <span><?php echo count( $services ); ?></span> <?php esc_html_e( 'Services', 'latepoint' ); ?>
914 </div>
915 <?php } ?>
916 </div>
917 </div>
918 <?php OsBookingHelper::generate_services_list( $services, false ); ?>
919 <?php OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) ); ?>
920 </div><?php
921 }
922 }
923 }
924 }
925 if ( ! $settings['preselected_category'] && ! $parent_id ) {
926 $category_bundles = $settings['bundles'];
927 if ( null === $category_bundles ) {
928 $bundles_model = new OsBundleModel();
929 $category_bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
930 /** This filter is documented in lib/helpers/steps_helper.php */
931 $category_bundles = apply_filters( 'latepoint_step_bundles', $category_bundles, [], [ 'service_display_mode' => $settings['service_display_mode'] ] );
932 }
933 OsBookingHelper::generate_bundles_folder( $category_bundles, [ 'service_display_mode' => $settings['service_display_mode'] ] );
934 }
935 if ( ! $settings['preselected_category'] ) {
936 echo '</div>';
937 }
938 }
939
940 public static function group_booking_btn_html( $booking_id = false ) {
941 $html = 'data-os-params="' . esc_attr( http_build_query( [ 'booking_id' => $booking_id ] ) ) . '"
942 data-os-action="' . esc_attr( OsRouterHelper::build_route_name( 'bookings', 'grouped_bookings_quick_view' ) ) . '"
943 data-os-output-target="lightbox"
944 data-os-lightbox-classes="width-500"
945 data-os-after-call="latepoint_init_grouped_bookings_form"';
946
947 return $html;
948 }
949
950 public static function quick_booking_btn_html( $booking_id = false, $params = array() ) {
951 $html = '';
952 if ( $booking_id ) {
953 $params['booking_id'] = $booking_id;
954 }
955 $route = OsRouterHelper::build_route_name( 'orders', 'quick_edit' );
956
957 $params_str = http_build_query( $params );
958 $html = 'data-os-params="' . esc_attr( $params_str ) . '"
959 data-os-action="' . esc_attr( $route ) . '"
960 data-os-output-target="side-panel"
961 data-os-after-call="latepoint_init_quick_order_form"';
962
963 return $html;
964 }
965
966
967 /**
968 * @param OsBookingModel $booking
969 *
970 * @return false|mixed
971 *
972 * Search for available location based on booking requirements. Will return false if no available location found.
973 */
974 public static function get_any_location_for_booking_by_rule( OsBookingModel $booking ) {
975 // ANY LOCATION SELECTED
976 // get available locations
977 $connected_ids = OsLocationHelper::get_location_ids_for_service_and_agent( $booking->service_id, $booking->agent_id );
978
979 // If date/time is selected - filter locations who are available at that time
980 if ( $booking->start_date && $booking->start_time ) {
981 $available_location_ids = [];
982 $booking_request = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
983 foreach ( $connected_ids as $location_id ) {
984 $booking_request->location_id = $location_id;
985 if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
986 $available_location_ids[] = $location_id;
987 }
988 }
989 $connected_ids = array_intersect( $available_location_ids, $connected_ids );
990 }
991
992
993 $locations_model = new OsLocationModel();
994 if ( ! empty( $connected_ids ) ) {
995 $locations_model->where_in( 'id', $connected_ids );
996 $locations = $locations_model->should_be_active()->get_results_as_models();
997 } else {
998 $locations = [];
999 }
1000
1001 if ( empty( $locations ) ) {
1002 return false;
1003 }
1004
1005 $selected_location_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
1006 $booking->location_id = $selected_location_id;
1007
1008 return $selected_location_id;
1009 }
1010
1011 /**
1012 * @param OsBookingModel $booking
1013 *
1014 * @return false|mixed
1015 *
1016 * Search for available agent based on booking requirements and agent picking preferences. Will return false if no available agent found.
1017 */
1018 public static function get_any_agent_for_booking_by_rule( OsBookingModel $booking ) {
1019 // ANY AGENT SELECTED
1020 // get available agents
1021 $connected_ids = OsAgentHelper::get_agent_ids_for_service_and_location( $booking->service_id, $booking->location_id );
1022
1023 // If date/time is selected - filter agents who are available at that time
1024 if ( $booking->start_date && $booking->start_time ) {
1025 $available_agent_ids = [];
1026 $booking_request = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
1027 foreach ( $connected_ids as $agent_id ) {
1028 $booking_request->agent_id = $agent_id;
1029 if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
1030 $available_agent_ids[] = $agent_id;
1031 }
1032 }
1033 $connected_ids = array_intersect( $available_agent_ids, $connected_ids );
1034 }
1035
1036
1037 /**
1038 * Get IDs of agents that are eligible to be assigned a booking that has "ANY" agent pre-selected
1039 *
1040 * @param {array} $connected_ids Array of eligible Agent IDs
1041 * @param {OsBookingModel} $booking Booking that needs agent ID
1042 *
1043 * @returns {array} Filtered array of IDs of eligible agents
1044 * @since 4.7.6
1045 * @hook latepoint_agent_ids_assignable_to_any_agent_booking
1046 *
1047 */
1048 $connected_ids = apply_filters( 'latepoint_agent_ids_assignable_to_any_agent_booking', $connected_ids, $booking );
1049
1050 if ( ! empty( $connected_ids ) ) {
1051 $agents_model = new OsAgentModel();
1052 $agents_model->where_in( 'id', $connected_ids );
1053 $agents = $agents_model->should_be_active()->get_results_as_models();
1054 } else {
1055 $agents = [];
1056 }
1057
1058 if ( empty( $agents ) ) {
1059 return false;
1060 }
1061
1062
1063 $selected_agent_id = false;
1064 $agent_order_rule = OsSettingsHelper::get_any_agent_order();
1065 switch ( $agent_order_rule ) {
1066 case LATEPOINT_ANY_AGENT_ORDER_RANDOM:
1067 $selected_agent_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
1068 break;
1069 case LATEPOINT_ANY_AGENT_ORDER_PRICE_HIGH:
1070 $highest_price = false;
1071 foreach ( $agents as $agent ) {
1072 $booking->agent_id = $agent->id;
1073 $price = OsBookingHelper::calculate_full_amount_for_booking( $booking );
1074 if ( $highest_price === false && $selected_agent_id === false ) {
1075 $highest_price = $price;
1076 $selected_agent_id = $agent->id;
1077 } else {
1078 if ( $highest_price < $price ) {
1079 $highest_price = $price;
1080 $selected_agent_id = $agent->id;
1081 }
1082 }
1083 }
1084 break;
1085 case LATEPOINT_ANY_AGENT_ORDER_PRICE_LOW:
1086 $lowest_price = false;
1087 foreach ( $agents as $agent ) {
1088 $booking->agent_id = $agent->id;
1089 $price = OsBookingHelper::calculate_full_amount_for_booking( $booking );
1090 if ( $lowest_price === false && $selected_agent_id === false ) {
1091 $lowest_price = $price;
1092 $selected_agent_id = $agent->id;
1093 } else {
1094 if ( $lowest_price > $price ) {
1095 $lowest_price = $price;
1096 $selected_agent_id = $agent->id;
1097 }
1098 }
1099 }
1100 break;
1101 case LATEPOINT_ANY_AGENT_ORDER_BUSY_HIGH:
1102 $max_bookings = false;
1103 foreach ( $agents as $agent ) {
1104 $agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
1105 if ( $max_bookings === false && $selected_agent_id === false ) {
1106 $max_bookings = $agent_total_bookings;
1107 $selected_agent_id = $agent->id;
1108 } else {
1109 if ( $max_bookings < $agent_total_bookings ) {
1110 $max_bookings = $agent_total_bookings;
1111 $selected_agent_id = $agent->id;
1112 }
1113 }
1114 }
1115 break;
1116 case LATEPOINT_ANY_AGENT_ORDER_BUSY_LOW:
1117 $min_bookings = false;
1118 foreach ( $agents as $agent ) {
1119 $agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
1120 if ( $min_bookings === false && $selected_agent_id === false ) {
1121 $min_bookings = $agent_total_bookings;
1122 $selected_agent_id = $agent->id;
1123 } else {
1124 if ( $min_bookings > $agent_total_bookings ) {
1125 $min_bookings = $agent_total_bookings;
1126 $selected_agent_id = $agent->id;
1127 }
1128 }
1129 }
1130 break;
1131 }
1132 /**
1133 * Get ID of agent that will be assigned to a booking, depending on order rules, where agent is set to ANY
1134 *
1135 * @param {integer} $selected_agent_id Currently selected agent ID
1136 * @param {OsAgentModel[]} $agents Array of eligible agent models to pick from
1137 * @param {OsBookingModel} $booking Booking that needs agent ID
1138 * @param {string} $agent_order_rule Rule of agent ordering
1139 *
1140 * @returns {integer} ID of the agent that will be assigned to this booking
1141 * @since 4.7.6
1142 * @hook latepoint_get_any_agent_id_for_booking_by_rule
1143 *
1144 */
1145 $selected_agent_id = apply_filters( 'latepoint_get_any_agent_id_for_booking_by_rule', $selected_agent_id, $agents, $booking, $agent_order_rule );
1146 $booking->agent_id = $selected_agent_id;
1147
1148 return $selected_agent_id;
1149 }
1150
1151
1152 public static function get_total_bookings_for_date( $date, $conditions = [], $grouped = false ) {
1153 $args = [ 'start_date' => $date ];
1154 if ( isset( $conditions['agent_id'] ) && $conditions['agent_id'] ) {
1155 $args['agent_id'] = $conditions['agent_id'];
1156 }
1157 if ( isset( $conditions['service_id'] ) && $conditions['service_id'] ) {
1158 $args['service_id'] = $conditions['service_id'];
1159 }
1160 if ( isset( $conditions['location_id'] ) && $conditions['location_id'] ) {
1161 $args['location_id'] = $conditions['location_id'];
1162 }
1163
1164
1165 $bookings = new OsBookingModel();
1166 if ( $grouped ) {
1167 $bookings->group_by( 'start_date, start_time, end_time, service_id, location_id' );
1168 }
1169 $bookings = $bookings->where( $args );
1170
1171 return $bookings->count();
1172 }
1173
1174
1175 /**
1176 *
1177 * Get list of statuses that block timeslot availability
1178 *
1179 * @return array
1180 */
1181 public static function get_timeslot_blocking_statuses(): array {
1182 $statuses = explode( ',', OsSettingsHelper::get_settings_value( 'timeslot_blocking_statuses', '' ) );
1183
1184 /**
1185 * Get list of statuses that block timeslot availability
1186 *
1187 * @param {array} $statuses array of status codes that block timeslot availability
1188 * @returns {array} The filtered array of status codes
1189 *
1190 * @since 4.7.0
1191 * @hook latepoint_get_timeslot_blocking_statuses
1192 *
1193 */
1194 return apply_filters( 'latepoint_get_timeslot_blocking_statuses', $statuses );
1195 }
1196
1197
1198 /**
1199 *
1200 * Get list of statuses that appear on pending page
1201 *
1202 * @return array
1203 */
1204 public static function get_booking_statuses_for_pending_page(): array {
1205 $statuses = explode( ',', OsSettingsHelper::get_settings_value( 'need_action_statuses', '' ) );
1206
1207 /**
1208 * Get list of statuses that appear on pending page
1209 *
1210 * @param {array} $statuses array of status codes that appear on pending page
1211 * @returns {array} The filtered array of status codes
1212 *
1213 * @since 4.7.0
1214 * @hook latepoint_get_booking_statuses_for_pending_page
1215 *
1216 */
1217 return apply_filters( 'latepoint_get_booking_statuses_for_pending_page', $statuses );
1218 }
1219
1220 /**
1221 *
1222 * Get list of statuses that are not cancelled
1223 *
1224 * @return array
1225 */
1226 public static function get_non_cancelled_booking_statuses(): array {
1227 $statuses = self::get_statuses_list();
1228 if ( isset( $statuses[ LATEPOINT_BOOKING_STATUS_CANCELLED ] ) ) {
1229 unset( $statuses[ LATEPOINT_BOOKING_STATUS_CANCELLED ] );
1230 }
1231 $statuses = array_keys( $statuses );
1232
1233 /**
1234 * Get list of statuses that are not cancelled
1235 *
1236 * @param {array} $statuses array of status codes that are not cancelled
1237 * @returns {array} The filtered array of status codes
1238 *
1239 * @since 5.0.5
1240 * @hook get_non_cancelled_booking_statuses
1241 *
1242 */
1243 return apply_filters( 'get_non_cancelled_booking_statuses', $statuses );
1244 }
1245
1246
1247 public static function get_default_booking_status( $service_id = false ) {
1248 if ( $service_id ) {
1249 $service = new OsServiceModel( $service_id );
1250 if ( $service && ! empty( $service->id ) ) {
1251 return $service->get_default_booking_status();
1252 }
1253 }
1254 $default_status = OsSettingsHelper::get_settings_value( 'default_booking_status' );
1255 if ( $default_status ) {
1256 return $default_status;
1257 } else {
1258 return LATEPOINT_BOOKING_STATUS_APPROVED;
1259 }
1260 }
1261
1262
1263 public static function change_booking_status( $booking_id, $new_status ) {
1264 $booking = new OsBookingModel( $booking_id );
1265 if ( ! $booking_id || ! $booking ) {
1266 return false;
1267 }
1268
1269 if ( $new_status == $booking->status ) {
1270 return true;
1271 } else {
1272 $old_booking = clone $booking;
1273 if ( $booking->update_status( $new_status ) ) {
1274 do_action( 'latepoint_booking_updated', $booking, $old_booking );
1275
1276 return true;
1277 } else {
1278 return false;
1279 }
1280 }
1281 }
1282
1283
1284 /**
1285 * @param \LatePoint\Misc\BookingRequest $booking_request
1286 * @param \LatePoint\Misc\BookedPeriod[]
1287 * @param int $capacity
1288 *
1289 * @return bool
1290 */
1291 public static function is_timeframe_in_booked_periods( \LatePoint\Misc\BookingRequest $booking_request, array $booked_periods, OsServiceModel $service ): bool {
1292 if ( empty( $booked_periods ) ) {
1293 return false;
1294 }
1295 $count_existing_attendees = 0;
1296 foreach ( $booked_periods as $period ) {
1297 if ( self::is_period_overlapping( $booking_request->get_start_time_with_buffer(), $booking_request->get_end_time_with_buffer(), $period->start_time_with_buffer(), $period->end_time_with_buffer() ) ) {
1298 // if it's the same service overlapping - count how many times
1299 // TODO maybe add an option to toggle on/off ability to share a timeslot capacity between two different services
1300 if ( $booking_request->service_id == $period->service_id ) {
1301 $count_existing_attendees += $period->total_attendees;
1302 } else {
1303 return true;
1304 }
1305 }
1306 }
1307 if ( $count_existing_attendees > 0 ) {
1308 // if there are attendees, check if they are below minimum need for timeslot to be blocked, if they are - then the slot is considered booked
1309 if ( ( $count_existing_attendees + $booking_request->total_attendees ) <= $service->get_capacity_needed_before_slot_is_blocked() ) {
1310 return false;
1311 } else {
1312 return true;
1313 }
1314 } else {
1315 // no attendees in the overlapping booked periods yet, just check if the requested number of attendees is within the service capacity
1316 if ( $booking_request->total_attendees <= $service->capacity_max ) {
1317 return false;
1318 } else {
1319 return true;
1320 }
1321 }
1322 }
1323
1324
1325 public static function is_period_overlapping( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
1326 // https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap/
1327 return ( ( $period_one_start < $period_two_end ) && ( $period_two_start < $period_one_end ) );
1328 }
1329
1330 public static function is_period_inside_another( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
1331 return ( ( $period_one_start >= $period_two_start ) && ( $period_one_end <= $period_two_end ) );
1332 }
1333
1334 // args = [agent_id, 'service_id', 'location_id']
1335 public static function get_bookings_for_date( $date, $args = [] ) {
1336 $bookings = new OsBookingModel();
1337 $args['start_date'] = $date;
1338 // if any of these are false or 0 - remove it from arguments list
1339 if ( isset( $args['location_id'] ) && empty( $args['location_id'] ) ) {
1340 unset( $args['location_id'] );
1341 }
1342 if ( isset( $args['agent_id'] ) && empty( $args['agent_id'] ) ) {
1343 unset( $args['agent_id'] );
1344 }
1345 if ( isset( $args['service_id'] ) && empty( $args['service_id'] ) ) {
1346 unset( $args['service_id'] );
1347 }
1348
1349 $bookings->where( $args )->order_by( 'start_time asc, end_time asc, service_id asc' );
1350
1351 return $bookings->get_results_as_models();
1352 }
1353
1354 /**
1355 * @param \LatePoint\Misc\Filter $filter
1356 *
1357 * @return int
1358 */
1359 public static function count_bookings( \LatePoint\Misc\Filter $filter ) {
1360 $bookings = new OsBookingModel();
1361 $query_args = [];
1362 if ( $filter->date_from ) {
1363 $query_args['start_date'] = $filter->date_from;
1364 }
1365 if ( $filter->location_id ) {
1366 $query_args['location_id'] = $filter->location_id;
1367 }
1368 if ( $filter->agent_id ) {
1369 $query_args['agent_id'] = $filter->agent_id;
1370 }
1371 if ( $filter->service_id ) {
1372 $query_args['service_id'] = $filter->service_id;
1373 }
1374
1375 return $bookings->should_not_be_cancelled()->where( $query_args )->count();
1376 }
1377
1378
1379 public static function get_nice_status_name( $status ) {
1380 $statuses_list = OsBookingHelper::get_statuses_list();
1381 if ( $status && isset( $statuses_list[ $status ] ) ) {
1382 return $statuses_list[ $status ];
1383 } else {
1384 return __( 'Undefined Status', 'latepoint' );
1385 }
1386 }
1387
1388
1389 public static function get_statuses_list() {
1390 $statuses = [
1391 LATEPOINT_BOOKING_STATUS_APPROVED => __( 'Approved', 'latepoint' ),
1392 LATEPOINT_BOOKING_STATUS_PENDING => __( 'Pending Approval', 'latepoint' ),
1393 LATEPOINT_BOOKING_STATUS_CANCELLED => __( 'Cancelled', 'latepoint' ),
1394 LATEPOINT_BOOKING_STATUS_NO_SHOW => __( 'No Show', 'latepoint' ),
1395 LATEPOINT_BOOKING_STATUS_COMPLETED => __( 'Completed', 'latepoint' ),
1396 ];
1397 $additional_statuses = array_map( 'trim', explode( ',', OsSettingsHelper::get_settings_value( 'additional_booking_statuses', '' ) ) );
1398 if ( ! empty( $additional_statuses ) ) {
1399 foreach ( $additional_statuses as $status ) {
1400 if ( ! empty( $status ) ) {
1401 $statuses[ str_replace( ' ', '_', strtolower( $status ) ) ] = $status;
1402 }
1403 }
1404 }
1405 $statuses = apply_filters( 'latepoint_booking_statuses', $statuses );
1406
1407 return $statuses;
1408 }
1409
1410
1411 public static function get_weekdays_arr( $full_name = false ) {
1412 if ( $full_name ) {
1413 $weekdays = array(
1414 __( 'Monday', 'latepoint' ),
1415 __( 'Tuesday', 'latepoint' ),
1416 __( 'Wednesday', 'latepoint' ),
1417 __( 'Thursday', 'latepoint' ),
1418 __( 'Friday', 'latepoint' ),
1419 __( 'Saturday', 'latepoint' ),
1420 __( 'Sunday', 'latepoint' ),
1421 );
1422 } else {
1423 $weekdays = array(
1424 __( 'Mon', 'latepoint' ),
1425 __( 'Tue', 'latepoint' ),
1426 __( 'Wed', 'latepoint' ),
1427 __( 'Thu', 'latepoint' ),
1428 __( 'Fri', 'latepoint' ),
1429 __( 'Sat', 'latepoint' ),
1430 __( 'Sun', 'latepoint' ),
1431 );
1432 }
1433
1434 return $weekdays;
1435 }
1436
1437 public static function get_weekday_name_by_number( $weekday_number, $full_name = false ) {
1438 $weekdays = OsBookingHelper::get_weekdays_arr( $full_name );
1439 if ( ! isset( $weekday_number ) || $weekday_number < 1 || $weekday_number > 7 ) {
1440 return '';
1441 } else {
1442 return $weekdays[ $weekday_number - 1 ];
1443 }
1444 }
1445
1446 public static function get_stat( $stat, $args = [] ) {
1447 if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
1448 return false;
1449 }
1450 $defaults = [
1451 'customer_id' => false,
1452 'agent_id' => false,
1453 'service_id' => false,
1454 'location_id' => false,
1455 'date_from' => false,
1456 'date_to' => false,
1457 'group_by' => false,
1458 'exclude_status' => false,
1459 ];
1460 $args = array_merge( $defaults, $args );
1461 $bookings = new OsBookingModel();
1462 $query_args = array( $args['date_from'], $args['date_to'] );
1463 switch ( $stat ) {
1464 case 'duration':
1465 $stat_query = 'SUM(end_time - start_time)';
1466 break;
1467 case 'price':
1468 $stat_query = 'sum(total)';
1469 break;
1470 case 'bookings':
1471 $stat_query = 'count(id)';
1472 break;
1473 }
1474 $select_query = $stat_query . ' as stat';
1475 if ( $args['group_by'] ) {
1476 $select_query .= ',' . $args['group_by'];
1477 }
1478 $bookings->select( $select_query );
1479
1480
1481 if ( $args['date_from'] ) {
1482 $bookings->where( [ 'start_date >=' => $args['date_from'] ] );
1483 }
1484 if ( $args['date_to'] ) {
1485 $bookings->where( [ 'start_date <=' => $args['date_to'] ] );
1486 }
1487 if ( $args['service_id'] ) {
1488 $bookings->where( [ 'service_id' => $args['service_id'] ] );
1489 }
1490 if ( $args['agent_id'] ) {
1491 $bookings->where( [ 'agent_id' => $args['agent_id'] ] );
1492 }
1493 if ( $args['location_id'] ) {
1494 $bookings->where( [ 'location_id' => $args['location_id'] ] );
1495 }
1496 if ( $args['customer_id'] ) {
1497 $bookings->where( [ 'customer_id' => $args['customer_id'] ] );
1498 }
1499 if ( $args['group_by'] ) {
1500 $bookings->group_by( $args['group_by'] );
1501 }
1502 // TODO, need to support custom status exclusions
1503 if ( $args['exclude_status'] == LATEPOINT_BOOKING_STATUS_CANCELLED ) {
1504 $bookings->should_not_be_cancelled();
1505 }
1506
1507 $stat_total = $bookings->get_results( ARRAY_A );
1508 if ( $args['group_by'] ) {
1509 return $stat_total;
1510 } else {
1511 return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
1512 }
1513 }
1514
1515 public static function get_new_customer_stat_for_period( DateTime $date_from, DateTime $date_to, \LatePoint\Misc\Filter $filter ) {
1516 // TODO make sure filter is respected
1517 $customers = new OsCustomerModel();
1518
1519 return $customers->filter_allowed_records()->where(
1520 [
1521 'created_at >=' => $date_from->format( 'Y-m-d' ),
1522 'created_at <=' => $date_to->format( 'Y-m-d' ),
1523 ]
1524 )->count();
1525 }
1526
1527 public static function get_stat_for_period( $stat, $date_from, $date_to, \LatePoint\Misc\Filter $filter, $group_by = false ) {
1528 if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
1529 return false;
1530 }
1531 if ( ! in_array( $group_by, [ false, 'agent_id', 'service_id', 'location_id' ] ) ) {
1532 return false;
1533 }
1534 $bookings = new OsBookingModel();
1535 switch ( $stat ) {
1536 case 'duration':
1537 $stat_query = 'SUM(end_time - start_time)';
1538 break;
1539 case 'price':
1540 $stat_query = 'sum(' . LATEPOINT_TABLE_ORDER_ITEMS . '.subtotal)';
1541 $bookings->join( LATEPOINT_TABLE_ORDER_ITEMS, [ 'id' => $bookings->table_name . '.order_item_id' ] );
1542 $bookings->join( LATEPOINT_TABLE_ORDERS, [ 'id' => LATEPOINT_TABLE_ORDER_ITEMS . '.order_id' ] );
1543 break;
1544 case 'bookings':
1545 $stat_query = 'count(id)';
1546 break;
1547 }
1548 $select_query = $stat_query . ' as stat';
1549 if ( $group_by ) {
1550 $select_query .= ',' . $group_by;
1551 }
1552 $bookings->select( $select_query )->where(
1553 [
1554 'start_date >=' => $date_from,
1555 'start_date <= ' => $date_to,
1556 ]
1557 );
1558
1559 if ( $filter->service_id ) {
1560 $bookings->where( [ 'service_id' => $filter->service_id ] );
1561 }
1562 if ( $filter->agent_id ) {
1563 $bookings->where( [ 'agent_id' => $filter->agent_id ] );
1564 }
1565 if ( $filter->location_id ) {
1566 $bookings->where( [ 'location_id' => $filter->location_id ] );
1567 }
1568
1569 $bookings->should_not_be_cancelled();
1570
1571 if ( $group_by ) {
1572 $bookings->group_by( $group_by );
1573 }
1574
1575 $stat_total = $bookings->get_results( ARRAY_A );
1576 if ( $group_by ) {
1577 return $stat_total;
1578 } else {
1579 return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
1580 }
1581 }
1582
1583 public static function get_total_bookings_per_day_for_period( $date_from, $date_to, \LatePoint\Misc\Filter $filter ) {
1584 $bookings = new OsBookingModel();
1585 $bookings->select( 'count(id) as bookings_per_day, start_date' )
1586 ->where(
1587 [
1588 'start_date >=' => $date_from,
1589 'start_date <=' => $date_to,
1590 ]
1591 )
1592 ->where( [ 'status NOT IN' => OsCalendarHelper::get_booking_statuses_hidden_from_calendar() ] );
1593 if ( $filter->service_id ) {
1594 $bookings->where( [ 'service_id' => $filter->service_id ] );
1595 }
1596 if ( $filter->agent_id ) {
1597 $bookings->where( [ 'agent_id' => $filter->agent_id ] );
1598 }
1599 if ( $filter->location_id ) {
1600 $bookings->where( [ 'location_id' => $filter->location_id ] );
1601 }
1602 $bookings->group_by( 'start_date' );
1603
1604 return $bookings->get_results();
1605 }
1606
1607
1608 public static function get_min_max_work_periods( $specific_weekdays = false, $service_id = false, $agent_id = false ) {
1609 $select_string = 'MIN(start_time) as start_time, MAX(end_time) as end_time';
1610 $work_periods = new OsWorkPeriodModel();
1611 $work_periods = $work_periods->select( $select_string );
1612 $query_args = array(
1613 'service_id' => 0,
1614 'agent_id' => 0,
1615 );
1616 if ( $service_id ) {
1617 $query_args['service_id'] = $service_id;
1618 }
1619 if ( $agent_id ) {
1620 $query_args['agent_id'] = $agent_id;
1621 }
1622 if ( $specific_weekdays && ! empty( $specific_weekdays ) ) {
1623 $query_args['week_day'] = $specific_weekdays;
1624 }
1625 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1626 if ( ( $service_id || $agent_id ) && empty( $results['min_start_time'] ) ) {
1627 if ( $service_id && empty( $results['min_start_time'] ) ) {
1628 $query_args['service_id'] = 0;
1629 $work_periods = new OsWorkPeriodModel();
1630 $work_periods = $work_periods->select( $select_string );
1631 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1632 }
1633 if ( $agent_id && empty( $results['min_start_time'] ) ) {
1634 $query_args['agent_id'] = 0;
1635 $work_periods = new OsWorkPeriodModel();
1636 $work_periods = $work_periods->select( $select_string );
1637 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1638 }
1639 }
1640 if ( $results ) {
1641 return array( $results['start_time'], $results['end_time'] );
1642 } else {
1643 return false;
1644 }
1645 }
1646
1647
1648 public static function get_work_start_end_time_for_multiple_dates( $dates = false, $service_id = false, $agent_id = false ) {
1649 $specific_weekdays = array();
1650 if ( $dates ) {
1651 foreach ( $dates as $date ) {
1652 $target_date = new OsWpDateTime( $date );
1653 $weekday = $target_date->format( 'N' );
1654 if ( ! in_array( $weekday, $specific_weekdays ) ) {
1655 $specific_weekdays[] = $weekday;
1656 }
1657 }
1658 }
1659 $work_minmax_start_end = self::get_min_max_work_periods( $specific_weekdays, $service_id, $agent_id );
1660
1661 return $work_minmax_start_end;
1662 }
1663
1664 /**
1665 * @param int $minute
1666 * @param \LatePoint\Misc\WorkPeriod[] $work_periods_arr
1667 *
1668 * @return bool
1669 */
1670 public static function is_minute_in_work_periods( int $minute, array $work_periods_arr ): bool {
1671 // print_r($work_periods_arr);
1672 if ( empty( $work_periods_arr ) ) {
1673 return false;
1674 }
1675 foreach ( $work_periods_arr as $work_period ) {
1676 // end of period does not count because we cant make appointment with 0 duration
1677 if ( $work_period->start_time <= $minute && $work_period->end_time > $minute ) {
1678 return true;
1679 }
1680 }
1681
1682 return false;
1683 }
1684
1685 public static function get_calendar_start_end_time( $bookings, $work_start_minutes, $work_end_minutes ) {
1686 $calendar_start_minutes = $work_start_minutes;
1687 $calendar_end_minutes = $work_end_minutes;
1688 if ( $bookings ) {
1689 foreach ( $bookings as $bookings_for_agent ) {
1690 if ( $bookings_for_agent ) {
1691 foreach ( $bookings_for_agent as $booking ) {
1692 if ( $booking->start_time < $calendar_start_minutes ) {
1693 $calendar_start_minutes = $booking->start_time;
1694 }
1695 if ( $booking->end_time > $calendar_end_minutes ) {
1696 $calendar_end_minutes = $booking->end_time;
1697 }
1698 }
1699 }
1700 }
1701 }
1702
1703 return [ $calendar_start_minutes, $calendar_end_minutes ];
1704 }
1705
1706 public static function generate_direct_manage_booking_url( OsBookingModel $booking, string $for ): string {
1707 if ( ! in_array( $for, [ 'agent', 'customer' ] ) ) {
1708 return '';
1709 }
1710 $key = $booking->get_key_to_manage_for( $for );
1711 $url = OsRouterHelper::build_admin_post_link( [ 'manage_booking_by_key', 'show' ], [ 'key' => $key ] );
1712
1713 return $url;
1714 }
1715
1716 public static function generate_summary_actions_for_booking( OsBookingModel $booking, ?string $key = null ) {
1717 ?>
1718 <div class="booking-full-summary-actions">
1719 <div class="add-to-calendar-wrapper">
1720 <a href="#" class="open-calendar-types booking-summary-action-btn"><i class="latepoint-icon latepoint-icon-calendar"></i><span><?php esc_html_e( 'Add to Calendar', 'latepoint' ); ?></span></a>
1721 <?php echo OsBookingHelper::generate_add_to_calendar_links( $booking, $key ?? $booking->get_key_to_manage_for( 'customer' ) ); ?>
1722 </div>
1723 <a href="<?php echo esc_url( $booking->get_print_link( $key ?? $booking->get_key_to_manage_for( 'customer' ) ) ); ?>" class="print-booking-btn booking-summary-action-btn" target="_blank"><i class="latepoint-icon latepoint-icon-printer"></i><span><?php esc_html_e( 'Print', 'latepoint' ); ?></span></a>
1724 <?php
1725 if ( $booking->is_upcoming() ) {
1726 if ( OsCustomerHelper::can_reschedule_booking( $booking ) ) { ?>
1727 <a href="#" class="latepoint-request-booking-reschedule booking-summary-action-btn" data-os-after-call="latepoint_init_reschedule" data-os-lightbox-classes="width-450 reschedule-calendar-wrapper" data-os-action="<?php echo esc_attr( OsRouterHelper::build_route_name( 'manage_booking_by_key', 'request_reschedule_calendar' ) ); ?>" data-os-params="<?php echo esc_attr( OsUtilHelper::build_os_params( [ 'key' => $key ?? $booking->get_key_to_manage_for( 'customer' ) ] ) ); ?>" data-os-output-target="lightbox">
1728 <i class="latepoint-icon latepoint-icon-calendar"></i>
1729 <span><?php esc_html_e( 'Reschedule', 'latepoint' ); ?></span>
1730 </a>
1731 <?php
1732 }
1733 if ( OsCustomerHelper::can_cancel_booking( $booking ) ) { ?>
1734 <a href="#" class="booking-summary-action-btn cancel-appointment-btn"
1735 data-os-prompt="<?php esc_attr_e( 'Are you sure you want to cancel this appointment?', 'latepoint' ); ?>"
1736 data-os-success-action="reload"
1737 data-os-action="<?php echo esc_attr( OsRouterHelper::build_route_name( 'manage_booking_by_key', 'request_cancellation' ) ); ?>"
1738 data-os-params="<?php echo esc_attr( OsUtilHelper::build_os_params( [ 'key' => $key ?? $booking->get_key_to_manage_for( 'customer' ) ], 'cancel_booking_' . $booking->id ) ); ?>">
1739 <i class="latepoint-icon latepoint-icon-ui-24"></i>
1740 <span><?php esc_html_e( 'Cancel', 'latepoint' ); ?></span>
1741 </a>
1742 <?php
1743 }
1744 }
1745 do_action( 'latepoint_booking_summary_after_booking_actions', $booking );
1746 ?>
1747 </div>
1748 <?php
1749 }
1750
1751 public static function generate_summary_for_booking( OsBookingModel $booking, $cart_item_id = false, ?string $viewer = 'customer' ): string {
1752 $summary_html = '';
1753 $summary_html .= apply_filters( 'latepoint_booking_summary_before_summary_box', '', $booking );
1754 $summary_html .= '<div class="summary-box main-box" ' . ( ( $cart_item_id ) ? 'data-cart-item-id="' . $cart_item_id . '"' : '' ) . '>';
1755 $output_timezone_name = $viewer == 'customer' ? $booking->get_customer_timezone_name() : OsTimeHelper::get_wp_timezone_name();
1756 if ( ! empty( $booking->start_datetime_utc ) ) {
1757 $summary_html .= '<div class="summary-box-booking-date-box">';
1758 $summary_html .= '<div class="summary-box-booking-date-day">' . $booking->start_datetime_in_format( 'j', $output_timezone_name ) . '</div>';
1759 $summary_html .= '<div class="summary-box-booking-date-month">' . OsUtilHelper::get_month_name_by_number( $booking->start_datetime_in_format( 'n', $output_timezone_name ), true ) . '</div>';
1760 $summary_html .= '</div>';
1761 }
1762 $summary_html .= '<div class="summary-box-inner">';
1763 $service_headings = [];
1764 $service_headings = apply_filters( 'latepoint_booking_summary_service_headings', $service_headings, $booking );
1765 if ( $service_headings ) {
1766 $summary_html .= '<div class="summary-box-heading">';
1767 foreach ( $service_headings as $heading ) {
1768 $summary_html .= '<div class="sbh-item">' . $heading . '</div>';
1769 }
1770 $summary_html .= '<div class="sbh-line"></div>';
1771 $summary_html .= '</div>';
1772 }
1773 $summary_html .= '<div class="summary-box-content os-cart-item">';
1774 if ( $cart_item_id && OsCartsHelper::can_checkout_multiple_items() ) {
1775 $summary_html .= '<div class="os-remove-item-from-cart" role="button" tabindex="0" data-confirm-text="' . __( 'Are you sure you want to remove this item from your cart?', 'latepoint' ) . '" data-cart-item-id="' . $cart_item_id . '" data-route="' . OsRouterHelper::build_route_name( 'carts', 'remove_item_from_cart' ) . '">
1776 <div class="os-remove-from-cart-icon"></div>
1777 </div>';
1778 }
1779 $summary_html .= '<div class="sbc-big-item">' . $booking->get_service_name_for_summary() . '</div>';
1780 if ( $booking->start_date ) {
1781 $summary_html .= '<div class="sbc-highlighted-item">' . $booking->get_nice_datetime_for_summary( $viewer ) . '</div>';
1782 }
1783 /**
1784 * Output summary of the booking data after a start date and time
1785 *
1786 * @since 5.2.0
1787 * @hook latepoint_summary_booking_info_after_start_date
1788 *
1789 * @param {string} $summary_html HTML of the summary
1790 * @param {OsBookingModel} $booking Booking object that is being outputted
1791 * @param {string} $cart_item_id ID of a cart item this booking belongs to
1792 * @param {string} $viewer determines who is viewing this summary, can be customer or agent
1793 *
1794 * @returns {string} Filtered HTML
1795 */
1796 $summary_html = apply_filters( 'latepoint_summary_booking_info_after_start_date', $summary_html, $booking, $cart_item_id, $viewer );
1797 $summary_html .= '</div>';
1798
1799 $service_attributes = [];
1800 $service_attributes = apply_filters( 'latepoint_booking_summary_service_attributes', $service_attributes, $booking );
1801 if ( $service_attributes ) {
1802 $summary_html .= '<div class="summary-attributes sa-clean">';
1803 foreach ( $service_attributes as $attribute ) {
1804 $summary_html .= '<span>' . $attribute['label'] . ': <strong>' . $attribute['value'] . '</strong></span>';
1805 }
1806 $summary_html .= '</div>';
1807 }
1808 $summary_html .= '</div>';
1809 $summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box_inner', '', $booking );
1810 $summary_html .= '</div>';
1811 $summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box', '', $booking );
1812
1813 return $summary_html;
1814 }
1815
1816
1817 /**
1818 * @param OsBookingModel[] $bookings
1819 *
1820 * @return bool
1821 */
1822 public static function bookings_have_same_agent( array $bookings ): bool {
1823 return ( count( array_unique( array_column( $bookings, 'agent_id' ) ) ) == 1 );
1824 }
1825
1826 /**
1827 * @param OsBookingModel[] $bookings
1828 *
1829 * @return bool
1830 */
1831 public static function bookings_have_same_location( array $bookings ): bool {
1832 return ( count( array_unique( array_column( $bookings, 'location_id' ) ) ) == 1 );
1833 }
1834
1835 /**
1836 * @param OsBookingModel[] $bookings
1837 *
1838 * @return bool
1839 */
1840 public static function bookings_have_same_service( array $bookings ): bool {
1841 return ( count( array_unique( array_column( $bookings, 'service_id' ) ) ) == 1 );
1842 }
1843
1844 public static function prepare_new_from_params( array $params ): OsBookingModel {
1845 $booking = new OsBookingModel();
1846
1847 $services = OsServiceHelper::get_allowed_active_services();
1848 $agents = OsAgentHelper::get_allowed_active_agents();
1849
1850 // LOAD FROM PASSED PARAMS
1851 $booking->order_item_id = $params['order_item_id'] ?? '';
1852 $booking->service_id = ! empty( $params['service_id'] ) ? OsUtilHelper::first_value_if_array( $params['service_id'] ) : '';
1853 if ( empty( $booking->service_id ) && ! empty( $services ) ) {
1854 $booking->service_id = $services[0]->id;
1855 }
1856
1857 $booking->agent_id = ! empty( $params['agent_id'] ) ? OsUtilHelper::first_value_if_array( $params['agent_id'] ) : '';
1858 if ( empty( $booking->agent_id ) && ! empty( $agents ) ) {
1859 $booking->agent_id = $agents[0]->id;
1860 }
1861
1862 if ( ! empty( $params['order_id'] ) ) {
1863 $order = new OsOrderModel( $params['order_id'] );
1864 $booking->customer_id = $order->customer_id;
1865 } else {
1866 $booking->customer_id = ! empty( $params['customer_id'] ) ? OsUtilHelper::first_value_if_array( $params['customer_id'] ) : '';
1867 }
1868
1869 $booking->location_id = ! empty( $params['location_id'] ) ? OsUtilHelper::first_value_if_array( $params['location_id'] ) : OsLocationHelper::get_default_location_id( true );
1870 $booking->start_date = $params['start_date'] ?? OsTimeHelper::today_date( 'Y-m-d' );
1871 $booking->start_time = $params['start_time'] ?? 600;
1872
1873 $booking->end_time = ( $booking->service_id ) ? $booking->calculate_end_time() : $booking->start_time + 60;
1874 $booking->end_date = $booking->calculate_end_date();
1875 $booking->buffer_before = ( $booking->service_id ) ? $booking->service->buffer_before : 0;
1876 $booking->buffer_after = ( $booking->service_id ) ? $booking->service->buffer_after : 0;
1877 $booking->status = LATEPOINT_BOOKING_STATUS_APPROVED;
1878
1879 return $booking;
1880 }
1881
1882 /**
1883 * Returns the <th> HTML for a single column header cell in the bookings table.
1884 *
1885 * @param string $col_key Unified column key (e.g. 'id', 'datetime', 'customer.email').
1886 * @param array $col_def Column definition from get_all_bookings_table_columns().
1887 * @param string $order_key Currently active sort key.
1888 * @param string $order_dir Currently active sort direction ('asc'|'desc').
1889 * @return string
1890 */
1891 public static function render_table_header_cell( string $col_key, array $col_def, string $order_key, string $order_dir ): string {
1892 switch ( $col_key ) {
1893 case 'id':
1894 $active_class = ( $order_key === 'booking_id' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1895 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_id">' . esc_html__( 'ID', 'latepoint' ) . '</th>';
1896
1897 case 'service':
1898 return '<th>' . esc_html__( 'Service', 'latepoint' ) . '</th>';
1899
1900 case 'datetime':
1901 $active_class = ( $order_key === 'booking_start_datetime' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1902 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_start_datetime">' . esc_html__( 'Date/Time', 'latepoint' ) . '</th>';
1903
1904 case 'time_left':
1905 $active_class = ( $order_key === 'booking_time_left' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1906 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_time_left">' . esc_html__( 'Time Left', 'latepoint' ) . '</th>';
1907
1908 case 'agent':
1909 return '<th>' . esc_html__( 'Agent', 'latepoint' ) . '</th>';
1910
1911 case 'location':
1912 return '<th>' . esc_html__( 'Location', 'latepoint' ) . '</th>';
1913
1914 case 'customer':
1915 return '<th>' . esc_html__( 'Customer', 'latepoint' ) . '</th>';
1916
1917 case 'status':
1918 return '<th>' . esc_html__( 'Status', 'latepoint' ) . '</th>';
1919
1920 case 'payment_status':
1921 return '<th>' . esc_html__( 'Payment Status', 'latepoint' ) . '</th>';
1922
1923 case 'created_on':
1924 $active_class = ( $order_key === 'booking_created_on' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1925 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_created_on">' . esc_html__( 'Created On', 'latepoint' ) . '</th>';
1926
1927 default:
1928 // Extra columns.
1929 return '<th>' . esc_html( $col_def['label'] ) . '</th>';
1930 }
1931 }
1932
1933 /**
1934 * Returns the <th> HTML for a single column filter cell in the bookings table header.
1935 *
1936 * @param string $col_key Unified column key.
1937 * @param array $col_def Column definition.
1938 * @param array $services_list Keyed list of services for the service filter select.
1939 * @param array $agents_list Keyed list of agents for the agent filter select.
1940 * @param array $locations_list Keyed list of locations for the location filter select.
1941 * @return string
1942 */
1943 public static function render_table_filter_cell( string $col_key, array $col_def, array $services_list, array $agents_list, array $locations_list ): string {
1944 switch ( $col_key ) {
1945 case 'id':
1946 return '<th>' . OsFormHelper::text_field(
1947 'filter[id]',
1948 false,
1949 '',
1950 [
1951 'placeholder' => __( 'ID', 'latepoint' ),
1952 'theme' => 'bordered',
1953 'style' => 'width: 60px;',
1954 'class' => 'os-table-filter',
1955 ]
1956 ) . '</th>';
1957
1958 case 'service':
1959 return '<th>' . OsFormHelper::select_field(
1960 'filter[service_id]',
1961 false,
1962 $services_list,
1963 '',
1964 [
1965 'placeholder' => __( 'All Services', 'latepoint' ),
1966 'class' => 'os-table-filter',
1967 ]
1968 ) . '</th>';
1969
1970 case 'datetime':
1971 ob_start();
1972 ?>
1973 <th>
1974 <div class="os-form-group">
1975 <div class="os-date-range-picker os-table-filter-datepicker" data-can-be-cleared="yes" data-no-value-label="<?php esc_attr_e( 'Search by Appointment Date', 'latepoint' ); ?>" data-clear-btn-label="<?php esc_attr_e( 'Reset Date Search', 'latepoint' ); ?>">
1976 <span class="range-picker-value"><?php esc_html_e( 'Filter Date', 'latepoint' ); ?></span>
1977 <i class="latepoint-icon latepoint-icon-chevron-down"></i>
1978 <input type="hidden" class="os-table-filter os-datepicker-date-from" name="filter[booking_date_from]" value=""/>
1979 <input type="hidden" class="os-table-filter os-datepicker-date-to" name="filter[booking_date_to]" value=""/>
1980 </div>
1981 </div>
1982 </th>
1983 <?php
1984 return ob_get_clean();
1985
1986 case 'time_left':
1987 return '<th>' . OsFormHelper::select_field(
1988 'filter[time_status]',
1989 false,
1990 [
1991 'upcoming' => __( 'Upcoming', 'latepoint' ),
1992 'past' => __( 'Past', 'latepoint' ),
1993 'now' => __( 'Happening Now', 'latepoint' ),
1994 ],
1995 '',
1996 [
1997 'placeholder' => __( 'Show All', 'latepoint' ),
1998 'class' => 'os-table-filter',
1999 ]
2000 ) . '</th>';
2001
2002 case 'agent':
2003 return '<th>' . OsFormHelper::select_field(
2004 'filter[agent_id]',
2005 false,
2006 $agents_list,
2007 '',
2008 [
2009 'placeholder' => __( 'All Agents', 'latepoint' ),
2010 'class' => 'os-table-filter',
2011 ]
2012 ) . '</th>';
2013
2014 case 'location':
2015 return '<th>' . OsFormHelper::select_field(
2016 'filter[location_id]',
2017 false,
2018 $locations_list,
2019 '',
2020 [
2021 'placeholder' => __( 'All Locations', 'latepoint' ),
2022 'class' => 'os-table-filter',
2023 ]
2024 ) . '</th>';
2025
2026 case 'customer':
2027 return '<th>' . OsFormHelper::text_field(
2028 'filter[customer][full_name]',
2029 false,
2030 '',
2031 [
2032 'class' => 'os-table-filter',
2033 'theme' => 'bordered',
2034 'placeholder' => __( 'Search by Customer', 'latepoint' ),
2035 ]
2036 ) . '</th>';
2037
2038 case 'status':
2039 return '<th>' . OsFormHelper::select_field(
2040 'filter[status]',
2041 false,
2042 OsBookingHelper::get_statuses_list(),
2043 '',
2044 [
2045 'placeholder' => __( 'Show All', 'latepoint' ),
2046 'class' => 'os-table-filter',
2047 ]
2048 ) . '</th>';
2049
2050 case 'payment_status':
2051 return '<th>' . OsFormHelper::select_field(
2052 'filter[order][payment_status]',
2053 false,
2054 OsOrdersHelper::get_order_payment_statuses_list(),
2055 '',
2056 [
2057 'placeholder' => __( 'Show All', 'latepoint' ),
2058 'class' => 'os-table-filter',
2059 ]
2060 ) . '</th>';
2061
2062 case 'created_on':
2063 ob_start();
2064 ?>
2065 <th>
2066 <div class="os-form-group">
2067 <div class="os-date-range-picker os-table-filter-datepicker" data-can-be-cleared="yes" data-no-value-label="<?php esc_attr_e( 'Filter Date', 'latepoint' ); ?>" data-clear-btn-label="<?php esc_attr_e( 'Reset Date Search', 'latepoint' ); ?>">
2068 <span class="range-picker-value"><?php esc_html_e( 'Filter Date', 'latepoint' ); ?></span>
2069 <i class="latepoint-icon latepoint-icon-chevron-down"></i>
2070 <input type="hidden" class="os-table-filter os-datepicker-date-from" name="filter[created_date_from]" value=""/>
2071 <input type="hidden" class="os-table-filter os-datepicker-date-to" name="filter[created_date_to]" value=""/>
2072 </div>
2073 </div>
2074 </th>
2075 <?php
2076 return ob_get_clean();
2077
2078 default:
2079 // Extra columns.
2080 if ( 'extra' !== $col_def['type'] ) {
2081 return '<th></th>';
2082 }
2083 $extra_type = $col_def['extra_type'];
2084 $extra_key = $col_def['extra_key'];
2085 $extra_label = $col_def['label'];
2086 $field_name = ( 'booking' !== $extra_type ) ? 'filter[' . $extra_type . '][' . $extra_key . ']' : 'filter[' . $extra_key . ']';
2087 // Skip the filter input for magic properties that can't be queried.
2088 if ( 'booking' === $extra_type && ! property_exists( 'OsBookingModel', $extra_key ) ) {
2089 return '<th></th>';
2090 }
2091 return '<th>' . OsFormHelper::text_field(
2092 $field_name,
2093 false,
2094 '',
2095 [
2096 'class' => 'os-table-filter',
2097 'theme' => 'bordered',
2098 'placeholder' => $extra_label,
2099 ]
2100 ) . '</th>';
2101 }
2102 }
2103
2104 /**
2105 * Returns the <td> HTML for a single data cell in the bookings table body.
2106 *
2107 * @param string $col_key Unified column key.
2108 * @param array $col_def Column definition.
2109 * @param OsBookingModel $booking Current booking row.
2110 * @return string
2111 */
2112 public static function render_table_body_cell( string $col_key, array $col_def, OsBookingModel $booking ): string {
2113 switch ( $col_key ) {
2114 case 'id':
2115 return '<td class="os-column-faded text-right has-floating-button">' .
2116 esc_html( $booking->id ) .
2117 '<div class="os-floating-button"><i class="latepoint-icon latepoint-icon-edit-3"></i></div>' .
2118 '</td>';
2119
2120 case 'service':
2121 return '<td><div class="os-with-service-color"><span class="cell-link-content">' .
2122 '<span class="os-column-service-color" style="background-color: ' . esc_attr( $booking->service->bg_color ) . '"></span>' .
2123 '<span>' . esc_html( $booking->service->name ) . '</span>' .
2124 '</span></div></td>';
2125
2126 case 'datetime':
2127 return '<td><strong>' . esc_html( $booking->nice_start_date ) . '</strong> <span class="os-dot"></span> <span>' . esc_html( $booking->nice_start_time ) . '</span></td>';
2128
2129 case 'time_left':
2130 $time_html = '';
2131 switch ( $booking->time_status() ) {
2132 case 'upcoming':
2133 $time_html = $booking->time_left;
2134 break;
2135 case 'now':
2136 $time_html = '<span class="time-left is-now">' . esc_html__( 'Now', 'latepoint' ) . '</span>';
2137 break;
2138 case 'past':
2139 $time_html = '<span class="time-left is-past">' . esc_html__( 'Past', 'latepoint' ) . '</span>';
2140 break;
2141 }
2142 return '<td><span class="in-table-time-left">' . $time_html . '</span></td>';
2143
2144 case 'agent':
2145 return '<td><div class="os-with-avatar">' .
2146 '<span class="cell-link-content">' .
2147 '<span class="os-avatar" style="background-image: url(' . esc_url( $booking->agent->get_avatar_url() ) . ')"></span>' .
2148 '<span class="os-name">' . esc_html( $booking->agent->full_name ) . '</span>' .
2149 '</span>' .
2150 '<div class="os-clickable-popup-trigger"' .
2151 ' data-route="' . esc_attr( OsRouterHelper::build_route_name( 'agents', 'mini_profile' ) ) . '"' .
2152 ' data-os-params="' . esc_attr(
2153 OsUtilHelper::build_os_params(
2154 [
2155 'agent_id' => $booking->agent_id,
2156 'booking_id' => $booking->id,
2157 ]
2158 )
2159 ) . '">' .
2160 '<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
2161 '</div>' .
2162 '</div></td>';
2163
2164 case 'location':
2165 return '<td>' . esc_html( $booking->location->name ) . '</td>';
2166
2167 case 'customer':
2168 return '<td><div class="os-with-avatar">' .
2169 '<span class="cell-link-content">' .
2170 '<span class="os-avatar" style="background-image: url(' . esc_url( $booking->customer->get_avatar_url() ) . ')"></span>' .
2171 '<span class="os-name">' . esc_html( $booking->customer->full_name ) . '</span>' .
2172 '</span>' .
2173 '<div class="os-clickable-popup-trigger"' .
2174 ' data-route="' . esc_attr( OsRouterHelper::build_route_name( 'customers', 'mini_profile' ) ) . '"' .
2175 ' data-os-params="' . esc_attr(
2176 OsUtilHelper::build_os_params(
2177 [
2178 'customer_id' => $booking->customer_id,
2179 'booking_id' => $booking->id,
2180 ]
2181 )
2182 ) . '">' .
2183 '<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
2184 '</div>' .
2185 '</div></td>';
2186
2187 case 'status':
2188 return '<td><span class="os-column-status os-column-status-' . esc_attr( $booking->status ) . '">' . esc_html( $booking->nice_status ) . '</span></td>';
2189
2190 case 'payment_status':
2191 return '<td><span class="os-column-status os-column-status-' . esc_attr( $booking->order->payment_status ) . '">' . esc_html( OsOrdersHelper::get_nice_order_payment_status_name( $booking->order->payment_status ) ) . '</span></td>';
2192
2193 case 'created_on':
2194 return '<td>' . esc_html( $booking->nice_created_at ) . '</td>';
2195
2196 default:
2197 // Extra columns: read from the appropriate model via property, getter, or meta.
2198 if ( 'extra' !== $col_def['type'] ) {
2199 return '<td></td>';
2200 }
2201 $extra_type = $col_def['extra_type'];
2202 $extra_key = $col_def['extra_key'];
2203 $model_obj = ( 'booking' === $extra_type ) ? $booking : $booking->$extra_type;
2204
2205 if ( property_exists( $model_obj, $extra_key ) || method_exists( $model_obj, 'get_' . $extra_key ) ) {
2206 return '<td>' . esc_html( $model_obj->$extra_key ) . '</td>';
2207 }
2208
2209 $column_value = $model_obj->get_meta_by_key( $extra_key );
2210 if ( $column_value &&
2211 ( $custom_fields_raw = OsSettingsHelper::get_settings_value( 'custom_fields_for_' . $extra_type, false ) ) &&
2212 ( $custom_fields_arr = json_decode( $custom_fields_raw, true ) ) &&
2213 isset( $custom_fields_arr[ $extra_key ]['type'] ) &&
2214 'multiselect' === $custom_fields_arr[ $extra_key ]['type']
2215 ) {
2216 $decoded = json_decode( $column_value );
2217 $column_value = is_array( $decoded ) ? implode( ', ', $decoded ) : $column_value;
2218 }
2219 return '<td>' . esc_html( $column_value ) . '</td>';
2220 }
2221 }
2222
2223 /**
2224 * Returns the <th> HTML for the bulk-selection column header (with the select-all checkbox).
2225 *
2226 * @return string
2227 */
2228 public static function render_bulk_select_header_cell(): string {
2229 return '<th class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
2230 '<label class="os-bulk-row-check-w">' .
2231 '<input type="checkbox" class="os-bulk-select-all" aria-label="' . esc_attr__( 'Select all appointments on this page', 'latepoint' ) . '" />' .
2232 '<span class="os-bulk-check-box"></span>' .
2233 '</label>' .
2234 '</th>';
2235 }
2236
2237 /**
2238 * Returns the empty spacer <th> HTML for the bulk-selection column in filter and footer rows.
2239 *
2240 * @return string
2241 */
2242 public static function render_bulk_select_spacer_cell(): string {
2243 return '<th class="os-bulk-select-cell"></th>';
2244 }
2245
2246 /**
2247 * Returns the <td> HTML for the bulk-selection column on a single body row.
2248 *
2249 * @param OsBookingModel $booking Current booking row.
2250 * @return string
2251 */
2252 public static function render_bulk_select_body_cell( OsBookingModel $booking ): string {
2253 /* translators: %d: appointment ID */
2254 $aria_label = sprintf( __( 'Select appointment %d', 'latepoint' ), $booking->id );
2255 return '<td class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
2256 '<label class="os-bulk-row-check-w">' .
2257 '<input type="checkbox" class="os-bulk-row-check" value="' . esc_attr( $booking->id ) . '" aria-label="' . esc_attr( $aria_label ) . '" />' .
2258 '<span class="os-bulk-check-box"></span>' .
2259 '</label>' .
2260 '</td>';
2261 }
2262
2263 /**
2264 * Returns the HTML for the bulk-actions toolbar shown above the appointments table.
2265 *
2266 * @return string
2267 */
2268 public static function render_bulk_actions_bar(): string {
2269 return '<div class="os-bulk-actions-bar" data-bulk-nonce="' . esc_attr( wp_create_nonce( 'bulk_destroy_bookings' ) ) . '">' .
2270 '<div class="os-bulk-actions-info">' .
2271 '<span class="os-bulk-selected-count">0</span>' .
2272 // Label is swapped between singular and plural by JS based on selection count.
2273 '<span class="os-bulk-selected-label">' . esc_html__( 'Appointments selected', 'latepoint' ) . '</span>' .
2274 '</div>' .
2275 '<div class="os-bulk-actions-controls">' .
2276 '<a href="#" class="latepoint-btn latepoint-btn-danger os-bulk-action-delete">' .
2277 '<i class="latepoint-icon latepoint-icon-trash-2"></i><span>' . esc_html__( 'Delete Selected', 'latepoint' ) . '</span>' .
2278 '</a>' .
2279 '<a href="#" class="os-bulk-actions-clear">' . esc_html__( 'Clear selection', 'latepoint' ) . '</a>' .
2280 '</div>' .
2281 '</div>';
2282 }
2283 }
2284