PluginProbe ʕ •ᴥ•ʔ
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress / 5.4.1
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress v5.4.1
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 3 months ago agent_helper.php 3 months ago analytics_helper.php 2 months ago auth_helper.php 3 months ago blocks_helper.php 3 months ago booking_helper.php 3 months ago bricks_helper.php 3 months ago bundles_helper.php 3 months ago calendar_helper.php 3 months ago carts_helper.php 3 months ago connector_helper.php 3 months ago csv_helper.php 3 months ago customer_helper.php 3 months ago customer_import_helper.php 3 months ago database_helper.php 3 months ago debug_helper.php 3 months ago defaults_helper.php 3 months ago elementor_helper.php 3 months ago email_helper.php 3 months ago encrypt_helper.php 3 months ago events_helper.php 3 months ago form_helper.php 3 months ago icalendar_helper.php 3 months ago image_helper.php 3 months ago invoices_helper.php 3 months ago license_helper.php 3 months ago location_helper.php 3 months ago marketing_systems_helper.php 3 months ago meeting_systems_helper.php 3 months ago menu_helper.php 2 months ago meta_helper.php 3 months ago migrations_helper.php 3 months ago money_helper.php 3 months ago notifications_helper.php 3 months ago nps_survey_helper.php 3 months ago order_intent_helper.php 3 months ago orders_helper.php 3 months ago otp_helper.php 3 months ago pages_helper.php 3 months ago params_helper.php 3 months ago payments_helper.php 3 months ago price_breakdown_helper.php 3 months ago process_jobs_helper.php 3 months ago processes_helper.php 3 months ago replacer_helper.php 3 months ago resource_helper.php 3 months ago roles_helper.php 3 months ago router_helper.php 3 months ago service_helper.php 3 months ago sessions_helper.php 3 months ago settings_helper.php 3 months ago short_links_systems_helper.php 3 months ago shortcodes_helper.php 2 months ago sms_helper.php 3 months ago steps_helper.php 3 months ago stripe_connect_helper.php 2 months ago styles_helper.php 3 months ago support_topics_helper.php 3 months ago time_helper.php 2 months ago timeline_helper.php 3 months ago transaction_helper.php 3 months ago transaction_intent_helper.php 3 months ago update_helper.php 2 months ago util_helper.php 3 months ago version_specific_updates_helper.php 3 months ago whatsapp_helper.php 3 months ago work_periods_helper.php 3 months ago wp_datetime.php 3 months ago wp_user_helper.php 3 months ago
booking_helper.php
1850 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(): void {
662 $bundles_model = new OsBundleModel();
663 $bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
664
665 if ( $bundles ) {
666 ?>
667 <div class="os-item-category-w os-items os-as-rows os-animated-child">
668 <div class="os-item-category-info-w os-item os-animated-self with-plus">
669 <div class="os-item-category-info os-item-i" tabindex="0">
670 <div class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></div>
671 <div class="os-item-name-w">
672 <div class="os-item-name"><?php echo esc_html__( 'Bundle & Save', 'latepoint' ); ?></div>
673 </div>
674 <?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $bundles ) ) { ?>
675 <div class="os-item-child-count">
676 <span><?php echo count( $bundles ); ?></span> <?php esc_html_e( 'Bundles', 'latepoint' ); ?>
677 </div>
678 <?php } ?>
679 </div>
680 </div>
681 <div class="os-bundles os-animated-parent os-items os-as-rows os-selectable-items">
682 <?php
683 foreach ( $bundles as $bundle ) { ?>
684 <div class="os-animated-child os-item os-selectable-item <?php echo ( $bundle->charge_amount ) ? 'os-priced-item' : ''; ?> <?php if ( $bundle->short_description ) {
685 echo 'with-description'; } ?>"
686 tabindex="0"
687 data-item-price="<?php echo esc_attr( $bundle->charge_amount ); ?>"
688 data-priced-item-type="bundle"
689 data-summary-field-name="bundle"
690 data-summary-value="<?php echo esc_attr( $bundle->name ); ?>"
691 data-item-id="<?php echo esc_attr( $bundle->id ); ?>"
692 data-cart-item-item-data-key="bundle_id"
693 data-os-call-func="latepoint_bundle_selected">
694 <div class="os-service-selector os-item-i os-animated-self"
695 data-bundle-id="<?php echo esc_attr( $bundle->id ); ?>">
696 <span class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></span>
697 <span class="os-item-name-w">
698 <span class="os-item-name"><?php echo esc_html( $bundle->name ); ?></span>
699 <?php if ( $bundle->short_description ) { ?>
700 <span class="os-item-desc"><?php echo wp_kses_post( $bundle->short_description ); ?></span>
701 <?php } ?>
702 </span>
703
704 <?php if ( $bundle->charge_amount > 0 ) { ?>
705 <span class="os-item-price-w">
706 <span class="os-item-price">
707 <?php echo esc_html( OsMoneyHelper::format_price( $bundle->charge_amount ) ); ?>
708 </span>
709 </span>
710 <?php } ?>
711 </div>
712 </div>
713 <?php
714 }
715 ?>
716 </div>
717 </div>
718 <?php
719 }
720 }
721
722 public static function generate_services_list( $services = false, $preselected_service = false ) {
723 if ( $services && is_array( $services ) && ! empty( $services ) ) { ?>
724 <div class="os-services os-animated-parent os-items os-as-rows os-selectable-items">
725 <?php foreach ( $services as $service ) {
726 // if service is preselected - only output that service, skip the rest
727 if ( $preselected_service && $service->id != $preselected_service->id ) {
728 continue;
729 }
730 $service_durations = $service->get_all_durations_arr();
731 $is_priced = ( ! ( count( $service_durations ) > 1 ) && $service->charge_amount ) ? true : false;
732 ?>
733 <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 ) {
734 echo 'with-description'; } ?>"
735 tabindex="0"
736 data-item-price="<?php echo esc_attr( $service->charge_amount ); ?>"
737 data-priced-item-type="service"
738 data-summary-field-name="service"
739 data-summary-value="<?php echo esc_attr( $service->name ); ?>"
740 data-item-id="<?php echo esc_attr( $service->id ); ?>"
741 data-cart-item-item-data-key="service_id"
742 data-os-call-func="latepoint_service_selected"
743 data-id-holder=".latepoint_service_id">
744 <div class="os-service-selector os-item-i os-animated-self" data-service-id="<?php echo esc_attr( $service->id ); ?>">
745 <?php if ( $service->selection_image_id ) { ?>
746 <span class="os-item-img-w" style="background-image: url(<?php echo esc_url( $service->selection_image_url ); ?>);"></span>
747 <?php } ?>
748 <span class="os-item-name-w">
749 <span class="os-item-name"><?php echo esc_html( $service->name ); ?></span>
750 <?php if ( $service->short_description ) { ?>
751 <span class="os-item-desc"><?php echo wp_kses_post( $service->short_description ); ?></span>
752 <?php } ?>
753 </span>
754 <?php if ( $service->price_min > 0 ) { ?>
755 <span class="os-item-price-w">
756 <span class="os-item-price">
757 <?php
758 /**
759 * Filters the display price value shown on the service tile on a booking form
760 *
761 * @since 5.1.94
762 * @hook latepoint_booking_form_display_service_price
763 *
764 * @param {string} $price displayed price that will be outputted
765 * @param {OsServiceModel} $service Service that the price is displayed for
766 *
767 * @returns {string} Filtered displayed price
768 */
769 $display_price = apply_filters( 'latepoint_booking_form_display_service_price', $service->price_min_formatted, $service );
770 echo esc_html( $display_price ) ?>
771 </span>
772 <?php if ( $service->price_min != $service->price_max ) { ?>
773 <span class="os-item-price-label"><?php esc_html_e( 'Starts From', 'latepoint' ); ?></span>
774 <?php } ?>
775 </span>
776 <?php } ?>
777 </div>
778 </div>
779 <?php } ?>
780 </div>
781 <?php }
782 }
783
784 public static function generate_services_bundles_and_categories_list( $parent_id = false, array $settings = [] ) {
785 $default_settings = [
786 'show_service_categories_arr' => false,
787 'show_services_arr' => false,
788 'preselected_service' => false,
789 'preselected_category' => false,
790 ];
791 $settings = array_merge( $default_settings, $settings );
792
793 if ( $settings['preselected_service'] ) {
794 OsBookingHelper::generate_services_list( [ $settings['preselected_service'] ], $settings['preselected_service'] );
795
796 return;
797 }
798
799 $service_categories = new OsServiceCategoryModel();
800 $args = array();
801 if ( $settings['show_service_categories_arr'] && is_array( $settings['show_service_categories_arr'] ) ) {
802 if ( $parent_id ) {
803 $service_categories->where( [ 'parent_id' => $parent_id ] );
804 } else {
805 if ( $settings['preselected_category'] ) {
806 $service_categories->where( [ 'id' => $settings['preselected_category'] ] );
807 } else {
808 $service_categories->where_in( 'id', $settings['show_service_categories_arr'] );
809 $service_categories->where(
810 [
811 'parent_id' => [
812 'OR' => [
813 'IS NULL',
814 ' NOT IN' => $settings['show_service_categories_arr'],
815 ],
816 ],
817 ]
818 );
819 }
820 }
821 } else {
822 if ( $settings['preselected_category'] ) {
823 $service_categories->where( [ 'id' => $settings['preselected_category'] ] );
824 } else {
825 $args['parent_id'] = $parent_id ? $parent_id : 'IS NULL';
826 }
827 }
828 $service_categories = $service_categories->where( $args )->order_by( 'order_number asc' )->get_results_as_models();
829
830 $main_parent_class = ( $parent_id ) ? 'os-animated-parent' : 'os-item-categories-main-parent os-animated-parent';
831 if ( ! $settings['preselected_category'] ) {
832 echo '<div class="os-item-categories-holder ' . esc_attr( $main_parent_class ) . '">';
833 }
834
835 // generate services that have no category
836 if ( $parent_id == false && $settings['preselected_category'] == false ) { ?>
837 <?php
838 $services_without_category = new OsServiceModel();
839 if ( $settings['show_services_arr'] ) {
840 $services_without_category->where_in( 'id', $settings['show_services_arr'] );
841 }
842 $services_without_category = $services_without_category->where( [ 'category_id' => 0 ] )->should_be_active()->should_not_be_hidden()->get_results_as_models();
843 if ( $services_without_category ) {
844 OsBookingHelper::generate_services_list( $services_without_category, false );
845 }
846 }
847
848 if ( is_array( $service_categories ) ) {
849 foreach ( $service_categories as $service_category ) { ?>
850 <?php
851 $services = [];
852 $category_services = $service_category->get_active_services();
853 if ( is_array( $category_services ) ) {
854 // if show selected services restriction is set - filter
855 if ( $settings['show_services_arr'] ) {
856 foreach ( $category_services as $category_service ) {
857 if ( in_array( $category_service->id, $settings['show_services_arr'] ) ) {
858 $services[] = $category_service;
859 }
860 }
861 } else {
862 $services = $category_services;
863 }
864 }
865 $child_categories = new OsServiceCategoryModel();
866 $count_child_categories = $child_categories->where( [ 'parent_id' => $service_category->id ] )->count();
867 // show only if it has either at least one child category or service
868 if ( $count_child_categories || count( $services ) ) {
869 // preselected category, just show contents, not the wrapper
870 if ( $service_category->id == $settings['preselected_category'] ) {
871 OsBookingHelper::generate_services_list( $services, false );
872 OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) );
873 } else { ?>
874 <div class="os-item-category-w os-items os-as-rows os-animated-child"
875 data-id="<?php echo esc_attr( $service_category->id ); ?>">
876 <div class="os-item-category-info-w os-item os-animated-self with-plus">
877 <div class="os-item-category-info os-item-i">
878 <div class="os-item-img-w"
879 style="background-image: url(<?php echo esc_url( $service_category->selection_image_url ); ?>);"></div>
880 <div class="os-item-name-w">
881 <div class="os-item-name"><?php echo esc_html( $service_category->name ); ?></div>
882 <?php if ( ! empty( $service_category->short_description ) ) { ?>
883 <div class="os-item-desc"><?php echo $service_category->short_description; ?></div>
884 <?php } ?>
885 </div>
886 <?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $services ) ) { ?>
887 <div class="os-item-child-count">
888 <span><?php echo count( $services ); ?></span> <?php esc_html_e( 'Services', 'latepoint' ); ?>
889 </div>
890 <?php } ?>
891 </div>
892 </div>
893 <?php OsBookingHelper::generate_services_list( $services, false ); ?>
894 <?php OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) ); ?>
895 </div><?php
896 }
897 }
898 }
899 }
900 if ( ! $settings['preselected_category'] && ! $parent_id ) {
901 OsBookingHelper::generate_bundles_folder();
902 }
903 if ( ! $settings['preselected_category'] ) {
904 echo '</div>';
905 }
906 }
907
908 public static function group_booking_btn_html( $booking_id = false ) {
909 $html = 'data-os-params="' . esc_attr( http_build_query( [ 'booking_id' => $booking_id ] ) ) . '"
910 data-os-action="' . esc_attr( OsRouterHelper::build_route_name( 'bookings', 'grouped_bookings_quick_view' ) ) . '"
911 data-os-output-target="lightbox"
912 data-os-lightbox-classes="width-500"
913 data-os-after-call="latepoint_init_grouped_bookings_form"';
914
915 return $html;
916 }
917
918 public static function quick_booking_btn_html( $booking_id = false, $params = array() ) {
919 $html = '';
920 if ( $booking_id ) {
921 $params['booking_id'] = $booking_id;
922 }
923 $route = OsRouterHelper::build_route_name( 'orders', 'quick_edit' );
924
925 $params_str = http_build_query( $params );
926 $html = 'data-os-params="' . esc_attr( $params_str ) . '"
927 data-os-action="' . esc_attr( $route ) . '"
928 data-os-output-target="side-panel"
929 data-os-after-call="latepoint_init_quick_order_form"';
930
931 return $html;
932 }
933
934
935 /**
936 * @param OsBookingModel $booking
937 *
938 * @return false|mixed
939 *
940 * Search for available location based on booking requirements. Will return false if no available location found.
941 */
942 public static function get_any_location_for_booking_by_rule( OsBookingModel $booking ) {
943 // ANY LOCATION SELECTED
944 // get available locations
945 $connected_ids = OsLocationHelper::get_location_ids_for_service_and_agent( $booking->service_id, $booking->agent_id );
946
947 // If date/time is selected - filter locations who are available at that time
948 if ( $booking->start_date && $booking->start_time ) {
949 $available_location_ids = [];
950 $booking_request = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
951 foreach ( $connected_ids as $location_id ) {
952 $booking_request->location_id = $location_id;
953 if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
954 $available_location_ids[] = $location_id;
955 }
956 }
957 $connected_ids = array_intersect( $available_location_ids, $connected_ids );
958 }
959
960
961 $locations_model = new OsLocationModel();
962 if ( ! empty( $connected_ids ) ) {
963 $locations_model->where_in( 'id', $connected_ids );
964 $locations = $locations_model->should_be_active()->get_results_as_models();
965 } else {
966 $locations = [];
967 }
968
969 if ( empty( $locations ) ) {
970 return false;
971 }
972
973 $selected_location_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
974 $booking->location_id = $selected_location_id;
975
976 return $selected_location_id;
977 }
978
979 /**
980 * @param OsBookingModel $booking
981 *
982 * @return false|mixed
983 *
984 * Search for available agent based on booking requirements and agent picking preferences. Will return false if no available agent found.
985 */
986 public static function get_any_agent_for_booking_by_rule( OsBookingModel $booking ) {
987 // ANY AGENT SELECTED
988 // get available agents
989 $connected_ids = OsAgentHelper::get_agent_ids_for_service_and_location( $booking->service_id, $booking->location_id );
990
991 // If date/time is selected - filter agents who are available at that time
992 if ( $booking->start_date && $booking->start_time ) {
993 $available_agent_ids = [];
994 $booking_request = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
995 foreach ( $connected_ids as $agent_id ) {
996 $booking_request->agent_id = $agent_id;
997 if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
998 $available_agent_ids[] = $agent_id;
999 }
1000 }
1001 $connected_ids = array_intersect( $available_agent_ids, $connected_ids );
1002 }
1003
1004
1005 /**
1006 * Get IDs of agents that are eligible to be assigned a booking that has "ANY" agent pre-selected
1007 *
1008 * @param {array} $connected_ids Array of eligible Agent IDs
1009 * @param {OsBookingModel} $booking Booking that needs agent ID
1010 *
1011 * @returns {array} Filtered array of IDs of eligible agents
1012 * @since 4.7.6
1013 * @hook latepoint_agent_ids_assignable_to_any_agent_booking
1014 *
1015 */
1016 $connected_ids = apply_filters( 'latepoint_agent_ids_assignable_to_any_agent_booking', $connected_ids, $booking );
1017
1018 if ( ! empty( $connected_ids ) ) {
1019 $agents_model = new OsAgentModel();
1020 $agents_model->where_in( 'id', $connected_ids );
1021 $agents = $agents_model->should_be_active()->get_results_as_models();
1022 } else {
1023 $agents = [];
1024 }
1025
1026 if ( empty( $agents ) ) {
1027 return false;
1028 }
1029
1030
1031 $selected_agent_id = false;
1032 $agent_order_rule = OsSettingsHelper::get_any_agent_order();
1033 switch ( $agent_order_rule ) {
1034 case LATEPOINT_ANY_AGENT_ORDER_RANDOM:
1035 $selected_agent_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
1036 break;
1037 case LATEPOINT_ANY_AGENT_ORDER_PRICE_HIGH:
1038 $highest_price = false;
1039 foreach ( $agents as $agent ) {
1040 $booking->agent_id = $agent->id;
1041 $price = OsBookingHelper::calculate_full_amount_for_booking( $booking );
1042 if ( $highest_price === false && $selected_agent_id === false ) {
1043 $highest_price = $price;
1044 $selected_agent_id = $agent->id;
1045 } else {
1046 if ( $highest_price < $price ) {
1047 $highest_price = $price;
1048 $selected_agent_id = $agent->id;
1049 }
1050 }
1051 }
1052 break;
1053 case LATEPOINT_ANY_AGENT_ORDER_PRICE_LOW:
1054 $lowest_price = false;
1055 foreach ( $agents as $agent ) {
1056 $booking->agent_id = $agent->id;
1057 $price = OsBookingHelper::calculate_full_amount_for_booking( $booking );
1058 if ( $lowest_price === false && $selected_agent_id === false ) {
1059 $lowest_price = $price;
1060 $selected_agent_id = $agent->id;
1061 } else {
1062 if ( $lowest_price > $price ) {
1063 $lowest_price = $price;
1064 $selected_agent_id = $agent->id;
1065 }
1066 }
1067 }
1068 break;
1069 case LATEPOINT_ANY_AGENT_ORDER_BUSY_HIGH:
1070 $max_bookings = false;
1071 foreach ( $agents as $agent ) {
1072 $agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
1073 if ( $max_bookings === false && $selected_agent_id === false ) {
1074 $max_bookings = $agent_total_bookings;
1075 $selected_agent_id = $agent->id;
1076 } else {
1077 if ( $max_bookings < $agent_total_bookings ) {
1078 $max_bookings = $agent_total_bookings;
1079 $selected_agent_id = $agent->id;
1080 }
1081 }
1082 }
1083 break;
1084 case LATEPOINT_ANY_AGENT_ORDER_BUSY_LOW:
1085 $min_bookings = false;
1086 foreach ( $agents as $agent ) {
1087 $agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
1088 if ( $min_bookings === false && $selected_agent_id === false ) {
1089 $min_bookings = $agent_total_bookings;
1090 $selected_agent_id = $agent->id;
1091 } else {
1092 if ( $min_bookings > $agent_total_bookings ) {
1093 $min_bookings = $agent_total_bookings;
1094 $selected_agent_id = $agent->id;
1095 }
1096 }
1097 }
1098 break;
1099 }
1100 /**
1101 * Get ID of agent that will be assigned to a booking, depending on order rules, where agent is set to ANY
1102 *
1103 * @param {integer} $selected_agent_id Currently selected agent ID
1104 * @param {OsAgentModel[]} $agents Array of eligible agent models to pick from
1105 * @param {OsBookingModel} $booking Booking that needs agent ID
1106 * @param {string} $agent_order_rule Rule of agent ordering
1107 *
1108 * @returns {integer} ID of the agent that will be assigned to this booking
1109 * @since 4.7.6
1110 * @hook latepoint_get_any_agent_id_for_booking_by_rule
1111 *
1112 */
1113 $selected_agent_id = apply_filters( 'latepoint_get_any_agent_id_for_booking_by_rule', $selected_agent_id, $agents, $booking, $agent_order_rule );
1114 $booking->agent_id = $selected_agent_id;
1115
1116 return $selected_agent_id;
1117 }
1118
1119
1120 public static function get_total_bookings_for_date( $date, $conditions = [], $grouped = false ) {
1121 $args = [ 'start_date' => $date ];
1122 if ( isset( $conditions['agent_id'] ) && $conditions['agent_id'] ) {
1123 $args['agent_id'] = $conditions['agent_id'];
1124 }
1125 if ( isset( $conditions['service_id'] ) && $conditions['service_id'] ) {
1126 $args['service_id'] = $conditions['service_id'];
1127 }
1128 if ( isset( $conditions['location_id'] ) && $conditions['location_id'] ) {
1129 $args['location_id'] = $conditions['location_id'];
1130 }
1131
1132
1133 $bookings = new OsBookingModel();
1134 if ( $grouped ) {
1135 $bookings->group_by( 'start_date, start_time, end_time, service_id, location_id' );
1136 }
1137 $bookings = $bookings->where( $args );
1138
1139 return $bookings->count();
1140 }
1141
1142
1143 /**
1144 *
1145 * Get list of statuses that block timeslot availability
1146 *
1147 * @return array
1148 */
1149 public static function get_timeslot_blocking_statuses(): array {
1150 $statuses = explode( ',', OsSettingsHelper::get_settings_value( 'timeslot_blocking_statuses', '' ) );
1151
1152 /**
1153 * Get list of statuses that block timeslot availability
1154 *
1155 * @param {array} $statuses array of status codes that block timeslot availability
1156 * @returns {array} The filtered array of status codes
1157 *
1158 * @since 4.7.0
1159 * @hook latepoint_get_timeslot_blocking_statuses
1160 *
1161 */
1162 return apply_filters( 'latepoint_get_timeslot_blocking_statuses', $statuses );
1163 }
1164
1165
1166 /**
1167 *
1168 * Get list of statuses that appear on pending page
1169 *
1170 * @return array
1171 */
1172 public static function get_booking_statuses_for_pending_page(): array {
1173 $statuses = explode( ',', OsSettingsHelper::get_settings_value( 'need_action_statuses', '' ) );
1174
1175 /**
1176 * Get list of statuses that appear on pending page
1177 *
1178 * @param {array} $statuses array of status codes that appear on pending page
1179 * @returns {array} The filtered array of status codes
1180 *
1181 * @since 4.7.0
1182 * @hook latepoint_get_booking_statuses_for_pending_page
1183 *
1184 */
1185 return apply_filters( 'latepoint_get_booking_statuses_for_pending_page', $statuses );
1186 }
1187
1188 /**
1189 *
1190 * Get list of statuses that are not cancelled
1191 *
1192 * @return array
1193 */
1194 public static function get_non_cancelled_booking_statuses(): array {
1195 $statuses = self::get_statuses_list();
1196 if ( isset( $statuses[ LATEPOINT_BOOKING_STATUS_CANCELLED ] ) ) {
1197 unset( $statuses[ LATEPOINT_BOOKING_STATUS_CANCELLED ] );
1198 }
1199 $statuses = array_keys( $statuses );
1200
1201 /**
1202 * Get list of statuses that are not cancelled
1203 *
1204 * @param {array} $statuses array of status codes that are not cancelled
1205 * @returns {array} The filtered array of status codes
1206 *
1207 * @since 5.0.5
1208 * @hook get_non_cancelled_booking_statuses
1209 *
1210 */
1211 return apply_filters( 'get_non_cancelled_booking_statuses', $statuses );
1212 }
1213
1214
1215 public static function get_default_booking_status( $service_id = false ) {
1216 if ( $service_id ) {
1217 $service = new OsServiceModel( $service_id );
1218 if ( $service && ! empty( $service->id ) ) {
1219 return $service->get_default_booking_status();
1220 }
1221 }
1222 $default_status = OsSettingsHelper::get_settings_value( 'default_booking_status' );
1223 if ( $default_status ) {
1224 return $default_status;
1225 } else {
1226 return LATEPOINT_BOOKING_STATUS_APPROVED;
1227 }
1228 }
1229
1230
1231 public static function change_booking_status( $booking_id, $new_status ) {
1232 $booking = new OsBookingModel( $booking_id );
1233 if ( ! $booking_id || ! $booking ) {
1234 return false;
1235 }
1236
1237 if ( $new_status == $booking->status ) {
1238 return true;
1239 } else {
1240 $old_booking = clone $booking;
1241 if ( $booking->update_status( $new_status ) ) {
1242 do_action( 'latepoint_booking_updated', $booking, $old_booking );
1243
1244 return true;
1245 } else {
1246 return false;
1247 }
1248 }
1249 }
1250
1251
1252 /**
1253 * @param \LatePoint\Misc\BookingRequest $booking_request
1254 * @param \LatePoint\Misc\BookedPeriod[]
1255 * @param int $capacity
1256 *
1257 * @return bool
1258 */
1259 public static function is_timeframe_in_booked_periods( \LatePoint\Misc\BookingRequest $booking_request, array $booked_periods, OsServiceModel $service ): bool {
1260 if ( empty( $booked_periods ) ) {
1261 return false;
1262 }
1263 $count_existing_attendees = 0;
1264 foreach ( $booked_periods as $period ) {
1265 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() ) ) {
1266 // if it's the same service overlapping - count how many times
1267 // TODO maybe add an option to toggle on/off ability to share a timeslot capacity between two different services
1268 if ( $booking_request->service_id == $period->service_id ) {
1269 $count_existing_attendees += $period->total_attendees;
1270 } else {
1271 return true;
1272 }
1273 }
1274 }
1275 if ( $count_existing_attendees > 0 ) {
1276 // 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
1277 if ( ( $count_existing_attendees + $booking_request->total_attendees ) <= $service->get_capacity_needed_before_slot_is_blocked() ) {
1278 return false;
1279 } else {
1280 return true;
1281 }
1282 } else {
1283 // no attendees in the overlapping booked periods yet, just check if the requested number of attendees is within the service capacity
1284 if ( $booking_request->total_attendees <= $service->capacity_max ) {
1285 return false;
1286 } else {
1287 return true;
1288 }
1289 }
1290 }
1291
1292
1293 public static function is_period_overlapping( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
1294 // https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap/
1295 return ( ( $period_one_start < $period_two_end ) && ( $period_two_start < $period_one_end ) );
1296 }
1297
1298 public static function is_period_inside_another( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
1299 return ( ( $period_one_start >= $period_two_start ) && ( $period_one_end <= $period_two_end ) );
1300 }
1301
1302 // args = [agent_id, 'service_id', 'location_id']
1303 public static function get_bookings_for_date( $date, $args = [] ) {
1304 $bookings = new OsBookingModel();
1305 $args['start_date'] = $date;
1306 // if any of these are false or 0 - remove it from arguments list
1307 if ( isset( $args['location_id'] ) && empty( $args['location_id'] ) ) {
1308 unset( $args['location_id'] );
1309 }
1310 if ( isset( $args['agent_id'] ) && empty( $args['agent_id'] ) ) {
1311 unset( $args['agent_id'] );
1312 }
1313 if ( isset( $args['service_id'] ) && empty( $args['service_id'] ) ) {
1314 unset( $args['service_id'] );
1315 }
1316
1317 $bookings->where( $args )->order_by( 'start_time asc, end_time asc, service_id asc' );
1318
1319 return $bookings->get_results_as_models();
1320 }
1321
1322 /**
1323 * @param \LatePoint\Misc\Filter $filter
1324 *
1325 * @return int
1326 */
1327 public static function count_bookings( \LatePoint\Misc\Filter $filter ) {
1328 $bookings = new OsBookingModel();
1329 $query_args = [];
1330 if ( $filter->date_from ) {
1331 $query_args['start_date'] = $filter->date_from;
1332 }
1333 if ( $filter->location_id ) {
1334 $query_args['location_id'] = $filter->location_id;
1335 }
1336 if ( $filter->agent_id ) {
1337 $query_args['agent_id'] = $filter->agent_id;
1338 }
1339 if ( $filter->service_id ) {
1340 $query_args['service_id'] = $filter->service_id;
1341 }
1342
1343 return $bookings->should_not_be_cancelled()->where( $query_args )->count();
1344 }
1345
1346
1347 public static function get_nice_status_name( $status ) {
1348 $statuses_list = OsBookingHelper::get_statuses_list();
1349 if ( $status && isset( $statuses_list[ $status ] ) ) {
1350 return $statuses_list[ $status ];
1351 } else {
1352 return __( 'Undefined Status', 'latepoint' );
1353 }
1354 }
1355
1356
1357 public static function get_statuses_list() {
1358 $statuses = [
1359 LATEPOINT_BOOKING_STATUS_APPROVED => __( 'Approved', 'latepoint' ),
1360 LATEPOINT_BOOKING_STATUS_PENDING => __( 'Pending Approval', 'latepoint' ),
1361 LATEPOINT_BOOKING_STATUS_CANCELLED => __( 'Cancelled', 'latepoint' ),
1362 LATEPOINT_BOOKING_STATUS_NO_SHOW => __( 'No Show', 'latepoint' ),
1363 LATEPOINT_BOOKING_STATUS_COMPLETED => __( 'Completed', 'latepoint' ),
1364 ];
1365 $additional_statuses = array_map( 'trim', explode( ',', OsSettingsHelper::get_settings_value( 'additional_booking_statuses', '' ) ) );
1366 if ( ! empty( $additional_statuses ) ) {
1367 foreach ( $additional_statuses as $status ) {
1368 if ( ! empty( $status ) ) {
1369 $statuses[ str_replace( ' ', '_', strtolower( $status ) ) ] = $status;
1370 }
1371 }
1372 }
1373 $statuses = apply_filters( 'latepoint_booking_statuses', $statuses );
1374
1375 return $statuses;
1376 }
1377
1378
1379 public static function get_weekdays_arr( $full_name = false ) {
1380 if ( $full_name ) {
1381 $weekdays = array(
1382 __( 'Monday', 'latepoint' ),
1383 __( 'Tuesday', 'latepoint' ),
1384 __( 'Wednesday', 'latepoint' ),
1385 __( 'Thursday', 'latepoint' ),
1386 __( 'Friday', 'latepoint' ),
1387 __( 'Saturday', 'latepoint' ),
1388 __( 'Sunday', 'latepoint' ),
1389 );
1390 } else {
1391 $weekdays = array(
1392 __( 'Mon', 'latepoint' ),
1393 __( 'Tue', 'latepoint' ),
1394 __( 'Wed', 'latepoint' ),
1395 __( 'Thu', 'latepoint' ),
1396 __( 'Fri', 'latepoint' ),
1397 __( 'Sat', 'latepoint' ),
1398 __( 'Sun', 'latepoint' ),
1399 );
1400 }
1401
1402 return $weekdays;
1403 }
1404
1405 public static function get_weekday_name_by_number( $weekday_number, $full_name = false ) {
1406 $weekdays = OsBookingHelper::get_weekdays_arr( $full_name );
1407 if ( ! isset( $weekday_number ) || $weekday_number < 1 || $weekday_number > 7 ) {
1408 return '';
1409 } else {
1410 return $weekdays[ $weekday_number - 1 ];
1411 }
1412 }
1413
1414 public static function get_stat( $stat, $args = [] ) {
1415 if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
1416 return false;
1417 }
1418 $defaults = [
1419 'customer_id' => false,
1420 'agent_id' => false,
1421 'service_id' => false,
1422 'location_id' => false,
1423 'date_from' => false,
1424 'date_to' => false,
1425 'group_by' => false,
1426 'exclude_status' => false,
1427 ];
1428 $args = array_merge( $defaults, $args );
1429 $bookings = new OsBookingModel();
1430 $query_args = array( $args['date_from'], $args['date_to'] );
1431 switch ( $stat ) {
1432 case 'duration':
1433 $stat_query = 'SUM(end_time - start_time)';
1434 break;
1435 case 'price':
1436 $stat_query = 'sum(total)';
1437 break;
1438 case 'bookings':
1439 $stat_query = 'count(id)';
1440 break;
1441 }
1442 $select_query = $stat_query . ' as stat';
1443 if ( $args['group_by'] ) {
1444 $select_query .= ',' . $args['group_by'];
1445 }
1446 $bookings->select( $select_query );
1447
1448
1449 if ( $args['date_from'] ) {
1450 $bookings->where( [ 'start_date >=' => $args['date_from'] ] );
1451 }
1452 if ( $args['date_to'] ) {
1453 $bookings->where( [ 'start_date <=' => $args['date_to'] ] );
1454 }
1455 if ( $args['service_id'] ) {
1456 $bookings->where( [ 'service_id' => $args['service_id'] ] );
1457 }
1458 if ( $args['agent_id'] ) {
1459 $bookings->where( [ 'agent_id' => $args['agent_id'] ] );
1460 }
1461 if ( $args['location_id'] ) {
1462 $bookings->where( [ 'location_id' => $args['location_id'] ] );
1463 }
1464 if ( $args['customer_id'] ) {
1465 $bookings->where( [ 'customer_id' => $args['customer_id'] ] );
1466 }
1467 if ( $args['group_by'] ) {
1468 $bookings->group_by( $args['group_by'] );
1469 }
1470 // TODO, need to support custom status exclusions
1471 if ( $args['exclude_status'] == LATEPOINT_BOOKING_STATUS_CANCELLED ) {
1472 $bookings->should_not_be_cancelled();
1473 }
1474
1475 $stat_total = $bookings->get_results( ARRAY_A );
1476 if ( $args['group_by'] ) {
1477 return $stat_total;
1478 } else {
1479 return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
1480 }
1481 }
1482
1483 public static function get_new_customer_stat_for_period( DateTime $date_from, DateTime $date_to, \LatePoint\Misc\Filter $filter ) {
1484 // TODO make sure filter is respected
1485 $customers = new OsCustomerModel();
1486
1487 return $customers->filter_allowed_records()->where(
1488 [
1489 'created_at >=' => $date_from->format( 'Y-m-d' ),
1490 'created_at <=' => $date_to->format( 'Y-m-d' ),
1491 ]
1492 )->count();
1493 }
1494
1495 public static function get_stat_for_period( $stat, $date_from, $date_to, \LatePoint\Misc\Filter $filter, $group_by = false ) {
1496 if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
1497 return false;
1498 }
1499 if ( ! in_array( $group_by, [ false, 'agent_id', 'service_id', 'location_id' ] ) ) {
1500 return false;
1501 }
1502 $bookings = new OsBookingModel();
1503 switch ( $stat ) {
1504 case 'duration':
1505 $stat_query = 'SUM(end_time - start_time)';
1506 break;
1507 case 'price':
1508 $stat_query = 'sum(' . LATEPOINT_TABLE_ORDER_ITEMS . '.subtotal)';
1509 $bookings->join( LATEPOINT_TABLE_ORDER_ITEMS, [ 'id' => $bookings->table_name . '.order_item_id' ] );
1510 $bookings->join( LATEPOINT_TABLE_ORDERS, [ 'id' => LATEPOINT_TABLE_ORDER_ITEMS . '.order_id' ] );
1511 break;
1512 case 'bookings':
1513 $stat_query = 'count(id)';
1514 break;
1515 }
1516 $select_query = $stat_query . ' as stat';
1517 if ( $group_by ) {
1518 $select_query .= ',' . $group_by;
1519 }
1520 $bookings->select( $select_query )->where(
1521 [
1522 'start_date >=' => $date_from,
1523 'start_date <= ' => $date_to,
1524 ]
1525 );
1526
1527 if ( $filter->service_id ) {
1528 $bookings->where( [ 'service_id' => $filter->service_id ] );
1529 }
1530 if ( $filter->agent_id ) {
1531 $bookings->where( [ 'agent_id' => $filter->agent_id ] );
1532 }
1533 if ( $filter->location_id ) {
1534 $bookings->where( [ 'location_id' => $filter->location_id ] );
1535 }
1536
1537 $bookings->should_not_be_cancelled();
1538
1539 if ( $group_by ) {
1540 $bookings->group_by( $group_by );
1541 }
1542
1543 $stat_total = $bookings->get_results( ARRAY_A );
1544 if ( $group_by ) {
1545 return $stat_total;
1546 } else {
1547 return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
1548 }
1549 }
1550
1551 public static function get_total_bookings_per_day_for_period( $date_from, $date_to, \LatePoint\Misc\Filter $filter ) {
1552 $bookings = new OsBookingModel();
1553 $bookings->select( 'count(id) as bookings_per_day, start_date' )
1554 ->where(
1555 [
1556 'start_date >=' => $date_from,
1557 'start_date <=' => $date_to,
1558 ]
1559 )
1560 ->where( [ 'status NOT IN' => OsCalendarHelper::get_booking_statuses_hidden_from_calendar() ] );
1561 if ( $filter->service_id ) {
1562 $bookings->where( [ 'service_id' => $filter->service_id ] );
1563 }
1564 if ( $filter->agent_id ) {
1565 $bookings->where( [ 'agent_id' => $filter->agent_id ] );
1566 }
1567 if ( $filter->location_id ) {
1568 $bookings->where( [ 'location_id' => $filter->location_id ] );
1569 }
1570 $bookings->group_by( 'start_date' );
1571
1572 return $bookings->get_results();
1573 }
1574
1575
1576 public static function get_min_max_work_periods( $specific_weekdays = false, $service_id = false, $agent_id = false ) {
1577 $select_string = 'MIN(start_time) as start_time, MAX(end_time) as end_time';
1578 $work_periods = new OsWorkPeriodModel();
1579 $work_periods = $work_periods->select( $select_string );
1580 $query_args = array(
1581 'service_id' => 0,
1582 'agent_id' => 0,
1583 );
1584 if ( $service_id ) {
1585 $query_args['service_id'] = $service_id;
1586 }
1587 if ( $agent_id ) {
1588 $query_args['agent_id'] = $agent_id;
1589 }
1590 if ( $specific_weekdays && ! empty( $specific_weekdays ) ) {
1591 $query_args['week_day'] = $specific_weekdays;
1592 }
1593 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1594 if ( ( $service_id || $agent_id ) && empty( $results['min_start_time'] ) ) {
1595 if ( $service_id && empty( $results['min_start_time'] ) ) {
1596 $query_args['service_id'] = 0;
1597 $work_periods = new OsWorkPeriodModel();
1598 $work_periods = $work_periods->select( $select_string );
1599 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1600 }
1601 if ( $agent_id && empty( $results['min_start_time'] ) ) {
1602 $query_args['agent_id'] = 0;
1603 $work_periods = new OsWorkPeriodModel();
1604 $work_periods = $work_periods->select( $select_string );
1605 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1606 }
1607 }
1608 if ( $results ) {
1609 return array( $results['start_time'], $results['end_time'] );
1610 } else {
1611 return false;
1612 }
1613 }
1614
1615
1616 public static function get_work_start_end_time_for_multiple_dates( $dates = false, $service_id = false, $agent_id = false ) {
1617 $specific_weekdays = array();
1618 if ( $dates ) {
1619 foreach ( $dates as $date ) {
1620 $target_date = new OsWpDateTime( $date );
1621 $weekday = $target_date->format( 'N' );
1622 if ( ! in_array( $weekday, $specific_weekdays ) ) {
1623 $specific_weekdays[] = $weekday;
1624 }
1625 }
1626 }
1627 $work_minmax_start_end = self::get_min_max_work_periods( $specific_weekdays, $service_id, $agent_id );
1628
1629 return $work_minmax_start_end;
1630 }
1631
1632 /**
1633 * @param int $minute
1634 * @param \LatePoint\Misc\WorkPeriod[] $work_periods_arr
1635 *
1636 * @return bool
1637 */
1638 public static function is_minute_in_work_periods( int $minute, array $work_periods_arr ): bool {
1639 // print_r($work_periods_arr);
1640 if ( empty( $work_periods_arr ) ) {
1641 return false;
1642 }
1643 foreach ( $work_periods_arr as $work_period ) {
1644 // end of period does not count because we cant make appointment with 0 duration
1645 if ( $work_period->start_time <= $minute && $work_period->end_time > $minute ) {
1646 return true;
1647 }
1648 }
1649
1650 return false;
1651 }
1652
1653 public static function get_calendar_start_end_time( $bookings, $work_start_minutes, $work_end_minutes ) {
1654 $calendar_start_minutes = $work_start_minutes;
1655 $calendar_end_minutes = $work_end_minutes;
1656 if ( $bookings ) {
1657 foreach ( $bookings as $bookings_for_agent ) {
1658 if ( $bookings_for_agent ) {
1659 foreach ( $bookings_for_agent as $booking ) {
1660 if ( $booking->start_time < $calendar_start_minutes ) {
1661 $calendar_start_minutes = $booking->start_time;
1662 }
1663 if ( $booking->end_time > $calendar_end_minutes ) {
1664 $calendar_end_minutes = $booking->end_time;
1665 }
1666 }
1667 }
1668 }
1669 }
1670
1671 return [ $calendar_start_minutes, $calendar_end_minutes ];
1672 }
1673
1674 public static function generate_direct_manage_booking_url( OsBookingModel $booking, string $for ): string {
1675 if ( ! in_array( $for, [ 'agent', 'customer' ] ) ) {
1676 return '';
1677 }
1678 $key = $booking->get_key_to_manage_for( $for );
1679 $url = OsRouterHelper::build_admin_post_link( [ 'manage_booking_by_key', 'show' ], [ 'key' => $key ] );
1680
1681 return $url;
1682 }
1683
1684 public static function generate_summary_actions_for_booking( OsBookingModel $booking, ?string $key = null ) {
1685 ?>
1686 <div class="booking-full-summary-actions">
1687 <div class="add-to-calendar-wrapper">
1688 <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>
1689 <?php echo OsBookingHelper::generate_add_to_calendar_links( $booking, $key ?? $booking->get_key_to_manage_for( 'customer' ) ); ?>
1690 </div>
1691 <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>
1692 <?php
1693 if ( $booking->is_upcoming() ) {
1694 if ( OsCustomerHelper::can_reschedule_booking( $booking ) ) { ?>
1695 <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">
1696 <i class="latepoint-icon latepoint-icon-calendar"></i>
1697 <span><?php esc_html_e( 'Reschedule', 'latepoint' ); ?></span>
1698 </a>
1699 <?php
1700 }
1701 if ( OsCustomerHelper::can_cancel_booking( $booking ) ) { ?>
1702 <a href="#" class="booking-summary-action-btn cancel-appointment-btn"
1703 data-os-prompt="<?php esc_attr_e( 'Are you sure you want to cancel this appointment?', 'latepoint' ); ?>"
1704 data-os-success-action="reload"
1705 data-os-action="<?php echo esc_attr( OsRouterHelper::build_route_name( 'manage_booking_by_key', 'request_cancellation' ) ); ?>"
1706 data-os-params="<?php echo esc_attr( OsUtilHelper::build_os_params( [ 'key' => $key ?? $booking->get_key_to_manage_for( 'customer' ) ], 'cancel_booking_' . $booking->id ) ); ?>">
1707 <i class="latepoint-icon latepoint-icon-ui-24"></i>
1708 <span><?php esc_html_e( 'Cancel', 'latepoint' ); ?></span>
1709 </a>
1710 <?php
1711 }
1712 }
1713 do_action( 'latepoint_booking_summary_after_booking_actions', $booking );
1714 ?>
1715 </div>
1716 <?php
1717 }
1718
1719 public static function generate_summary_for_booking( OsBookingModel $booking, $cart_item_id = false, ?string $viewer = 'customer' ): string {
1720 $summary_html = '';
1721 $summary_html .= apply_filters( 'latepoint_booking_summary_before_summary_box', '', $booking );
1722 $summary_html .= '<div class="summary-box main-box" ' . ( ( $cart_item_id ) ? 'data-cart-item-id="' . $cart_item_id . '"' : '' ) . '>';
1723 $output_timezone_name = $viewer == 'customer' ? $booking->get_customer_timezone_name() : OsTimeHelper::get_wp_timezone_name();
1724 if ( ! empty( $booking->start_datetime_utc ) ) {
1725 $summary_html .= '<div class="summary-box-booking-date-box">';
1726 $summary_html .= '<div class="summary-box-booking-date-day">' . $booking->start_datetime_in_format( 'j', $output_timezone_name ) . '</div>';
1727 $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>';
1728 $summary_html .= '</div>';
1729 }
1730 $summary_html .= '<div class="summary-box-inner">';
1731 $service_headings = [];
1732 $service_headings = apply_filters( 'latepoint_booking_summary_service_headings', $service_headings, $booking );
1733 if ( $service_headings ) {
1734 $summary_html .= '<div class="summary-box-heading">';
1735 foreach ( $service_headings as $heading ) {
1736 $summary_html .= '<div class="sbh-item">' . $heading . '</div>';
1737 }
1738 $summary_html .= '<div class="sbh-line"></div>';
1739 $summary_html .= '</div>';
1740 }
1741 $summary_html .= '<div class="summary-box-content os-cart-item">';
1742 if ( $cart_item_id && OsCartsHelper::can_checkout_multiple_items() ) {
1743 $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' ) . '">
1744 <div class="os-remove-from-cart-icon"></div>
1745 </div>';
1746 }
1747 $summary_html .= '<div class="sbc-big-item">' . $booking->get_service_name_for_summary() . '</div>';
1748 if ( $booking->start_date ) {
1749 $summary_html .= '<div class="sbc-highlighted-item">' . $booking->get_nice_datetime_for_summary( $viewer ) . '</div>';
1750 }
1751 /**
1752 * Output summary of the booking data after a start date and time
1753 *
1754 * @since 5.2.0
1755 * @hook latepoint_summary_booking_info_after_start_date
1756 *
1757 * @param {string} $summary_html HTML of the summary
1758 * @param {OsBookingModel} $booking Booking object that is being outputted
1759 * @param {string} $cart_item_id ID of a cart item this booking belongs to
1760 * @param {string} $viewer determines who is viewing this summary, can be customer or agent
1761 *
1762 * @returns {string} Filtered HTML
1763 */
1764 $summary_html = apply_filters( 'latepoint_summary_booking_info_after_start_date', $summary_html, $booking, $cart_item_id, $viewer );
1765 $summary_html .= '</div>';
1766
1767 $service_attributes = [];
1768 $service_attributes = apply_filters( 'latepoint_booking_summary_service_attributes', $service_attributes, $booking );
1769 if ( $service_attributes ) {
1770 $summary_html .= '<div class="summary-attributes sa-clean">';
1771 foreach ( $service_attributes as $attribute ) {
1772 $summary_html .= '<span>' . $attribute['label'] . ': <strong>' . $attribute['value'] . '</strong></span>';
1773 }
1774 $summary_html .= '</div>';
1775 }
1776 $summary_html .= '</div>';
1777 $summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box_inner', '', $booking );
1778 $summary_html .= '</div>';
1779 $summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box', '', $booking );
1780
1781 return $summary_html;
1782 }
1783
1784
1785 /**
1786 * @param OsBookingModel[] $bookings
1787 *
1788 * @return bool
1789 */
1790 public static function bookings_have_same_agent( array $bookings ): bool {
1791 return ( count( array_unique( array_column( $bookings, 'agent_id' ) ) ) == 1 );
1792 }
1793
1794 /**
1795 * @param OsBookingModel[] $bookings
1796 *
1797 * @return bool
1798 */
1799 public static function bookings_have_same_location( array $bookings ): bool {
1800 return ( count( array_unique( array_column( $bookings, 'location_id' ) ) ) == 1 );
1801 }
1802
1803 /**
1804 * @param OsBookingModel[] $bookings
1805 *
1806 * @return bool
1807 */
1808 public static function bookings_have_same_service( array $bookings ): bool {
1809 return ( count( array_unique( array_column( $bookings, 'service_id' ) ) ) == 1 );
1810 }
1811
1812 public static function prepare_new_from_params( array $params ): OsBookingModel {
1813 $booking = new OsBookingModel();
1814
1815 $services = OsServiceHelper::get_allowed_active_services();
1816 $agents = OsAgentHelper::get_allowed_active_agents();
1817
1818 // LOAD FROM PASSED PARAMS
1819 $booking->order_item_id = $params['order_item_id'] ?? '';
1820 $booking->service_id = ! empty( $params['service_id'] ) ? OsUtilHelper::first_value_if_array( $params['service_id'] ) : '';
1821 if ( empty( $booking->service_id ) && ! empty( $services ) ) {
1822 $booking->service_id = $services[0]->id;
1823 }
1824
1825 $booking->agent_id = ! empty( $params['agent_id'] ) ? OsUtilHelper::first_value_if_array( $params['agent_id'] ) : '';
1826 if ( empty( $booking->agent_id ) && ! empty( $agents ) ) {
1827 $booking->agent_id = $agents[0]->id;
1828 }
1829
1830 if ( ! empty( $params['order_id'] ) ) {
1831 $order = new OsOrderModel( $params['order_id'] );
1832 $booking->customer_id = $order->customer_id;
1833 } else {
1834 $booking->customer_id = ! empty( $params['customer_id'] ) ? OsUtilHelper::first_value_if_array( $params['customer_id'] ) : '';
1835 }
1836
1837 $booking->location_id = ! empty( $params['location_id'] ) ? OsUtilHelper::first_value_if_array( $params['location_id'] ) : OsLocationHelper::get_default_location_id( true );
1838 $booking->start_date = $params['start_date'] ?? OsTimeHelper::today_date( 'Y-m-d' );
1839 $booking->start_time = $params['start_time'] ?? 600;
1840
1841 $booking->end_time = ( $booking->service_id ) ? $booking->calculate_end_time() : $booking->start_time + 60;
1842 $booking->end_date = $booking->calculate_end_date();
1843 $booking->buffer_before = ( $booking->service_id ) ? $booking->service->buffer_before : 0;
1844 $booking->buffer_after = ( $booking->service_id ) ? $booking->service->buffer_after : 0;
1845 $booking->status = LATEPOINT_BOOKING_STATUS_APPROVED;
1846
1847 return $booking;
1848 }
1849 }
1850