PluginProbe ʕ •ᴥ•ʔ
LatePoint – Calendar Booking Plugin for Appointments and Events / 5.6.6
LatePoint – Calendar Booking Plugin for Appointments and Events v5.6.6
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 1 week ago auth_helper.php 6 days ago blocks_helper.php 3 weeks ago booking_helper.php 4 days ago bricks_helper.php 3 months ago bundles_helper.php 4 days ago calendar_helper.php 3 days ago carts_helper.php 3 months ago connector_helper.php 3 months ago csv_helper.php 3 months ago customer_helper.php 1 month ago customer_import_helper.php 1 month ago database_helper.php 1 week 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 weeks 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 3 weeks 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 2 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 2 months ago plugin_version_update_helper.php 2 months ago price_breakdown_helper.php 3 months ago process_jobs_helper.php 3 months ago processes_helper.php 3 months ago razorpay_connect_helper.php 1 week ago replacer_helper.php 3 months ago resource_helper.php 4 days ago roles_helper.php 3 weeks ago router_helper.php 3 months ago service_helper.php 3 months ago sessions_helper.php 3 months ago settings_helper.php 1 week ago short_links_systems_helper.php 3 months ago shortcodes_helper.php 3 weeks ago sms_helper.php 3 months ago steps_helper.php 1 week ago stripe_connect_helper.php 1 week ago styles_helper.php 3 months ago support_topics_helper.php 3 months ago time_helper.php 2 months ago timeline_helper.php 2 months ago transaction_helper.php 1 month ago transaction_intent_helper.php 3 months ago util_helper.php 1 month ago version_specific_updates_helper.php 3 months ago whatsapp_helper.php 1 month ago work_periods_helper.php 3 months ago wp_datetime.php 3 months ago wp_user_helper.php 3 months ago
booking_helper.php
2351 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, $accessed_from_backend );
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 /**
668 * Filters the list of bundles shown on the services step of the booking form.
669 *
670 * @since 5.6.3
671 * @hook latepoint_booking_generate_bundles_folder
672 *
673 * @param {OsBundleModel[]} $bundles List of active, non-hidden bundles to be displayed
674 *
675 * @returns {OsBundleModel[]} Filtered list of bundles to display
676 */
677 $bundles = apply_filters( 'latepoint_booking_generate_bundles_folder', $bundles );
678
679 if ( $bundles ) {
680 /**
681 * Whether to skip the "Bundle & Save" folder header and render bundle
682 * tiles directly (without a collapsible category wrapper).
683 *
684 * @param bool $skip Whether to skip the folder wrapper. Default false.
685 * @param array $restrictions Active booking-form restrictions.
686 *
687 * @hook latepoint_bundles_folder_skip_wrapper
688 */
689 $skip_folder_wrapper = (bool) apply_filters( 'latepoint_bundles_folder_skip_wrapper', false, $restrictions );
690
691 if ( ! $skip_folder_wrapper ) {
692 ?>
693 <div class="os-item-category-w os-items os-as-rows os-animated-child">
694 <div class="os-item-category-info-w os-item os-animated-self with-plus">
695 <div class="os-item-category-info os-item-i" tabindex="0">
696 <div class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></div>
697 <div class="os-item-name-w">
698 <div class="os-item-name"><?php echo esc_html__( 'Bundle & Save', 'latepoint' ); ?></div>
699 </div>
700 <?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $bundles ) ) { ?>
701 <div class="os-item-child-count">
702 <span><?php echo count( $bundles ); ?></span> <?php esc_html_e( 'Bundles', 'latepoint' ); ?>
703 </div>
704 <?php } ?>
705 </div>
706 </div>
707 <?php
708 }
709 ?>
710 <div class="os-bundles os-animated-parent os-items os-as-rows os-selectable-items">
711 <?php
712 foreach ( $bundles as $bundle ) { ?>
713 <div class="os-animated-child os-item os-selectable-item <?php echo ( $bundle->charge_amount ) ? 'os-priced-item' : ''; ?> <?php if ( $bundle->short_description ) {
714 echo 'with-description'; } ?>"
715 tabindex="0"
716 data-item-price="<?php echo esc_attr( $bundle->charge_amount ); ?>"
717 data-priced-item-type="bundle"
718 data-summary-field-name="bundle"
719 data-summary-value="<?php echo esc_attr( $bundle->name ); ?>"
720 data-item-id="<?php echo esc_attr( $bundle->id ); ?>"
721 data-cart-item-item-data-key="bundle_id"
722 data-os-call-func="latepoint_bundle_selected">
723 <div class="os-service-selector os-item-i os-animated-self"
724 data-bundle-id="<?php echo esc_attr( $bundle->id ); ?>">
725 <span class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></span>
726 <span class="os-item-name-w">
727 <span class="os-item-name"><?php echo esc_html( $bundle->name ); ?></span>
728 <?php if ( $bundle->short_description ) { ?>
729 <span class="os-item-desc"><?php echo wp_kses_post( $bundle->short_description ); ?></span>
730 <?php } ?>
731 </span>
732
733 <?php if ( $bundle->price_min > 0 ) { ?>
734 <span class="os-item-price-w">
735 <span class="os-item-price">
736 <?php
737 /**
738 * Filters the display price value shown on the bundle tile on a booking form
739 *
740 * @since 5.1.94
741 * @hook latepoint_booking_form_display_bundle_price
742 *
743 * @param {string} $price displayed price that will be outputted
744 * @param {OsBundleModel} $bundle Bundle that the price is displayed for
745 *
746 * @returns {string} Filtered displayed price
747 */
748 $display_price = apply_filters( 'latepoint_booking_form_display_bundle_price', $bundle->price_min_formatted, $bundle );
749 echo esc_html( $display_price ); ?>
750 </span>
751 <?php if ( $bundle->price_min != $bundle->price_max ) { ?>
752 <span class="os-item-price-label"><?php esc_html_e( 'Starts From', 'latepoint' ); ?></span>
753 <?php } ?>
754 </span>
755 <?php } elseif ( $bundle->charge_amount > 0 ) { ?>
756 <span class="os-item-price-w">
757 <span class="os-item-price">
758 <?php echo esc_html( OsMoneyHelper::format_price( $bundle->charge_amount ) ); ?>
759 </span>
760 </span>
761 <?php } ?>
762 </div>
763 </div>
764 <?php
765 }
766 ?>
767 </div>
768 <?php
769 if ( ! $skip_folder_wrapper ) {
770 echo '</div>';
771 }
772 }
773 }
774
775 public static function generate_services_list( $services = false, $preselected_service = false ) {
776 if ( $services && is_array( $services ) && ! empty( $services ) ) { ?>
777 <div class="os-services os-animated-parent os-items os-as-rows os-selectable-items">
778 <?php foreach ( $services as $service ) {
779 // if service is preselected - only output that service, skip the rest
780 if ( $preselected_service && $service->id != $preselected_service->id ) {
781 continue;
782 }
783 $service_durations = $service->get_all_durations_arr();
784 $is_priced = ( ! ( count( $service_durations ) > 1 ) && $service->charge_amount ) ? true : false;
785 ?>
786 <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 ) {
787 echo 'with-description'; } ?>"
788 tabindex="0"
789 data-item-price="<?php echo esc_attr( $service->charge_amount ); ?>"
790 data-priced-item-type="service"
791 data-summary-field-name="service"
792 data-summary-value="<?php echo esc_attr( $service->name ); ?>"
793 data-item-id="<?php echo esc_attr( $service->id ); ?>"
794 data-cart-item-item-data-key="service_id"
795 data-os-call-func="latepoint_service_selected"
796 data-id-holder=".latepoint_service_id">
797 <div class="os-service-selector os-item-i os-animated-self" data-service-id="<?php echo esc_attr( $service->id ); ?>">
798 <?php if ( $service->selection_image_id ) { ?>
799 <span class="os-item-img-w" style="background-image: url(<?php echo esc_url( $service->selection_image_url ); ?>);"></span>
800 <?php } ?>
801 <span class="os-item-name-w">
802 <span class="os-item-name"><?php echo esc_html( $service->name ); ?></span>
803 <?php if ( $service->short_description ) { ?>
804 <span class="os-item-desc"><?php echo wp_kses_post( $service->short_description ); ?></span>
805 <?php } ?>
806 </span>
807 <?php if ( $service->price_min > 0 ) { ?>
808 <span class="os-item-price-w">
809 <span class="os-item-price">
810 <?php
811 /**
812 * Filters the display price value shown on the service tile on a booking form
813 *
814 * @since 5.1.94
815 * @hook latepoint_booking_form_display_service_price
816 *
817 * @param {string} $price displayed price that will be outputted
818 * @param {OsServiceModel} $service Service that the price is displayed for
819 *
820 * @returns {string} Filtered displayed price
821 */
822 $display_price = apply_filters( 'latepoint_booking_form_display_service_price', $service->price_min_formatted, $service );
823 echo esc_html( $display_price ) ?>
824 </span>
825 <?php if ( $service->price_min != $service->price_max ) { ?>
826 <span class="os-item-price-label"><?php esc_html_e( 'Starts From', 'latepoint' ); ?></span>
827 <?php } ?>
828 </span>
829 <?php } ?>
830 </div>
831 </div>
832 <?php } ?>
833 </div>
834 <?php }
835 }
836
837 public static function generate_services_bundles_and_categories_list( $parent_id = false, array $settings = [] ) {
838 $default_settings = [
839 'show_service_categories_arr' => false,
840 'show_services_arr' => false,
841 'preselected_service' => false,
842 'preselected_category' => false,
843 'bundles' => null,
844 'service_display_mode' => 'all',
845 ];
846 $settings = array_merge( $default_settings, $settings );
847
848 if ( $settings['preselected_service'] ) {
849 OsBookingHelper::generate_services_list( [ $settings['preselected_service'] ], $settings['preselected_service'] );
850
851 return;
852 }
853
854 $service_categories = new OsServiceCategoryModel();
855 $args = array();
856 if ( $settings['show_service_categories_arr'] && is_array( $settings['show_service_categories_arr'] ) ) {
857 if ( $parent_id ) {
858 $service_categories->where( [ 'parent_id' => $parent_id ] );
859 } else {
860 if ( $settings['preselected_category'] ) {
861 $service_categories->where( [ 'id' => $settings['preselected_category'] ] );
862 } else {
863 $service_categories->where_in( 'id', $settings['show_service_categories_arr'] );
864 $service_categories->where(
865 [
866 'parent_id' => [
867 'OR' => [
868 'IS NULL',
869 ' NOT IN' => $settings['show_service_categories_arr'],
870 ],
871 ],
872 ]
873 );
874 }
875 }
876 } else {
877 if ( $settings['preselected_category'] ) {
878 $service_categories->where( [ 'id' => $settings['preselected_category'] ] );
879 } else {
880 $args['parent_id'] = $parent_id ? $parent_id : 'IS NULL';
881 }
882 }
883 $service_categories = $service_categories->where( $args )->order_by( 'order_number asc' )->get_results_as_models();
884
885 $main_parent_class = ( $parent_id ) ? 'os-animated-parent' : 'os-item-categories-main-parent os-animated-parent';
886 if ( ! $settings['preselected_category'] ) {
887 echo '<div class="os-item-categories-holder ' . esc_attr( $main_parent_class ) . '">';
888 }
889
890 // generate services that have no category
891 if ( $parent_id == false && $settings['preselected_category'] == false ) { ?>
892 <?php
893 $services_without_category = new OsServiceModel();
894 if ( $settings['show_services_arr'] ) {
895 $services_without_category->where_in( 'id', $settings['show_services_arr'] );
896 }
897 $services_without_category = $services_without_category->where( [ 'category_id' => 0 ] )->should_be_active()->should_not_be_hidden()->get_results_as_models();
898 /** This filter is documented in lib/helpers/steps_helper.php */
899 $services_without_category = apply_filters( 'latepoint_step_services', $services_without_category, $settings['bundles'], [ 'service_display_mode' => $settings['service_display_mode'] ] );
900 if ( $services_without_category ) {
901 OsBookingHelper::generate_services_list( $services_without_category, false );
902 }
903 }
904
905 if ( is_array( $service_categories ) ) {
906 foreach ( $service_categories as $service_category ) { ?>
907 <?php
908 $services = [];
909 $category_services = $service_category->get_active_services();
910 if ( is_array( $category_services ) ) {
911 // if show selected services restriction is set - filter
912 if ( $settings['show_services_arr'] ) {
913 foreach ( $category_services as $category_service ) {
914 if ( in_array( $category_service->id, $settings['show_services_arr'] ) ) {
915 $services[] = $category_service;
916 }
917 }
918 } else {
919 $services = $category_services;
920 }
921 }
922 /** This filter is documented in lib/helpers/steps_helper.php */
923 $services = apply_filters( 'latepoint_step_services', $services, $settings['bundles'], [ 'service_display_mode' => $settings['service_display_mode'] ] );
924 $child_categories = new OsServiceCategoryModel();
925 $count_child_categories = $child_categories->where( [ 'parent_id' => $service_category->id ] )->count();
926 // show only if it has either at least one child category or service
927 if ( $count_child_categories || count( $services ) ) {
928 // preselected category, just show contents, not the wrapper
929 if ( $service_category->id == $settings['preselected_category'] ) {
930 OsBookingHelper::generate_services_list( $services, false );
931 OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) );
932 } else { ?>
933 <div class="os-item-category-w os-items os-as-rows os-animated-child"
934 data-id="<?php echo esc_attr( $service_category->id ); ?>">
935 <div class="os-item-category-info-w os-item os-animated-self with-plus">
936 <div class="os-item-category-info os-item-i">
937 <div class="os-item-img-w"
938 style="background-image: url(<?php echo esc_url( $service_category->selection_image_url ); ?>);"></div>
939 <div class="os-item-name-w">
940 <div class="os-item-name"><?php echo esc_html( $service_category->name ); ?></div>
941 <?php if ( ! empty( $service_category->short_description ) ) { ?>
942 <div class="os-item-desc"><?php echo $service_category->short_description; ?></div>
943 <?php } ?>
944 </div>
945 <?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $services ) ) { ?>
946 <div class="os-item-child-count">
947 <span><?php echo count( $services ); ?></span> <?php esc_html_e( 'Services', 'latepoint' ); ?>
948 </div>
949 <?php } ?>
950 </div>
951 </div>
952 <?php OsBookingHelper::generate_services_list( $services, false ); ?>
953 <?php OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) ); ?>
954 </div><?php
955 }
956 }
957 }
958 }
959 if ( ! $settings['preselected_category'] && ! $parent_id ) {
960 $category_bundles = $settings['bundles'];
961 if ( null === $category_bundles ) {
962 $bundles_model = new OsBundleModel();
963 $category_bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
964 /** This filter is documented in lib/helpers/steps_helper.php */
965 $category_bundles = apply_filters( 'latepoint_step_bundles', $category_bundles, [], [ 'service_display_mode' => $settings['service_display_mode'] ] );
966 }
967 OsBookingHelper::generate_bundles_folder( $category_bundles, [ 'service_display_mode' => $settings['service_display_mode'] ] );
968 }
969 if ( ! $settings['preselected_category'] ) {
970 echo '</div>';
971 }
972 }
973
974 public static function group_booking_btn_html( $booking_id = false ) {
975 $html = 'data-os-params="' . esc_attr( http_build_query( [ 'booking_id' => $booking_id ] ) ) . '"
976 data-os-action="' . esc_attr( OsRouterHelper::build_route_name( 'bookings', 'grouped_bookings_quick_view' ) ) . '"
977 data-os-output-target="lightbox"
978 data-os-lightbox-classes="width-500"
979 data-os-after-call="latepoint_init_grouped_bookings_form"';
980
981 return $html;
982 }
983
984 public static function quick_booking_btn_html( $booking_id = false, $params = array() ) {
985 $html = '';
986 if ( $booking_id ) {
987 $params['booking_id'] = $booking_id;
988 }
989 $route = OsRouterHelper::build_route_name( 'orders', 'quick_edit' );
990
991 $params_str = http_build_query( $params );
992 $html = 'data-os-params="' . esc_attr( $params_str ) . '"
993 data-os-action="' . esc_attr( $route ) . '"
994 data-os-output-target="side-panel"
995 data-os-after-call="latepoint_init_quick_order_form"';
996
997 return $html;
998 }
999
1000
1001 /**
1002 * @param OsBookingModel $booking
1003 *
1004 * @return false|mixed
1005 *
1006 * Search for available location based on booking requirements. Will return false if no available location found.
1007 */
1008 public static function get_any_location_for_booking_by_rule( OsBookingModel $booking ) {
1009 // ANY LOCATION SELECTED
1010 // get available locations
1011 $connected_ids = OsLocationHelper::get_location_ids_for_service_and_agent( $booking->service_id, $booking->agent_id );
1012
1013 // If date/time is selected - filter locations who are available at that time
1014 if ( $booking->start_date && $booking->start_time ) {
1015 $available_location_ids = [];
1016 $booking_request = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
1017 foreach ( $connected_ids as $location_id ) {
1018 $booking_request->location_id = $location_id;
1019 if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
1020 $available_location_ids[] = $location_id;
1021 }
1022 }
1023 $connected_ids = array_intersect( $available_location_ids, $connected_ids );
1024 }
1025
1026
1027 $locations_model = new OsLocationModel();
1028 if ( ! empty( $connected_ids ) ) {
1029 $locations_model->where_in( 'id', $connected_ids );
1030 $locations = $locations_model->should_be_active()->get_results_as_models();
1031 } else {
1032 $locations = [];
1033 }
1034
1035 if ( empty( $locations ) ) {
1036 return false;
1037 }
1038
1039 $selected_location_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
1040 $booking->location_id = $selected_location_id;
1041
1042 return $selected_location_id;
1043 }
1044
1045 /**
1046 * @param OsBookingModel $booking
1047 *
1048 * @return false|mixed
1049 *
1050 * Search for available agent based on booking requirements and agent picking preferences. Will return false if no available agent found.
1051 */
1052 public static function get_any_agent_for_booking_by_rule( OsBookingModel $booking ) {
1053 // ANY AGENT SELECTED
1054 // get available agents
1055 $connected_ids = OsAgentHelper::get_agent_ids_for_service_and_location( $booking->service_id, $booking->location_id );
1056
1057 // If date/time is selected - filter agents who are available at that time
1058 if ( $booking->start_date && $booking->start_time ) {
1059 $available_agent_ids = [];
1060 $booking_request = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
1061 foreach ( $connected_ids as $agent_id ) {
1062 $booking_request->agent_id = $agent_id;
1063 if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
1064 $available_agent_ids[] = $agent_id;
1065 }
1066 }
1067 $connected_ids = array_intersect( $available_agent_ids, $connected_ids );
1068 }
1069
1070
1071 /**
1072 * Get IDs of agents that are eligible to be assigned a booking that has "ANY" agent pre-selected
1073 *
1074 * @param {array} $connected_ids Array of eligible Agent IDs
1075 * @param {OsBookingModel} $booking Booking that needs agent ID
1076 *
1077 * @returns {array} Filtered array of IDs of eligible agents
1078 * @since 4.7.6
1079 * @hook latepoint_agent_ids_assignable_to_any_agent_booking
1080 *
1081 */
1082 $connected_ids = apply_filters( 'latepoint_agent_ids_assignable_to_any_agent_booking', $connected_ids, $booking );
1083
1084 if ( ! empty( $connected_ids ) ) {
1085 $agents_model = new OsAgentModel();
1086 $agents_model->where_in( 'id', $connected_ids );
1087 $agents = $agents_model->should_be_active()->get_results_as_models();
1088 } else {
1089 $agents = [];
1090 }
1091
1092 if ( empty( $agents ) ) {
1093 return false;
1094 }
1095
1096
1097 $selected_agent_id = false;
1098 $agent_order_rule = OsSettingsHelper::get_any_agent_order();
1099 switch ( $agent_order_rule ) {
1100 case LATEPOINT_ANY_AGENT_ORDER_RANDOM:
1101 $selected_agent_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
1102 break;
1103 case LATEPOINT_ANY_AGENT_ORDER_PRICE_HIGH:
1104 $highest_price = false;
1105 foreach ( $agents as $agent ) {
1106 $booking->agent_id = $agent->id;
1107 $price = OsBookingHelper::calculate_full_amount_for_booking( $booking );
1108 if ( $highest_price === false && $selected_agent_id === false ) {
1109 $highest_price = $price;
1110 $selected_agent_id = $agent->id;
1111 } else {
1112 if ( $highest_price < $price ) {
1113 $highest_price = $price;
1114 $selected_agent_id = $agent->id;
1115 }
1116 }
1117 }
1118 break;
1119 case LATEPOINT_ANY_AGENT_ORDER_PRICE_LOW:
1120 $lowest_price = false;
1121 foreach ( $agents as $agent ) {
1122 $booking->agent_id = $agent->id;
1123 $price = OsBookingHelper::calculate_full_amount_for_booking( $booking );
1124 if ( $lowest_price === false && $selected_agent_id === false ) {
1125 $lowest_price = $price;
1126 $selected_agent_id = $agent->id;
1127 } else {
1128 if ( $lowest_price > $price ) {
1129 $lowest_price = $price;
1130 $selected_agent_id = $agent->id;
1131 }
1132 }
1133 }
1134 break;
1135 case LATEPOINT_ANY_AGENT_ORDER_BUSY_HIGH:
1136 $max_bookings = false;
1137 foreach ( $agents as $agent ) {
1138 $agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
1139 if ( $max_bookings === false && $selected_agent_id === false ) {
1140 $max_bookings = $agent_total_bookings;
1141 $selected_agent_id = $agent->id;
1142 } else {
1143 if ( $max_bookings < $agent_total_bookings ) {
1144 $max_bookings = $agent_total_bookings;
1145 $selected_agent_id = $agent->id;
1146 }
1147 }
1148 }
1149 break;
1150 case LATEPOINT_ANY_AGENT_ORDER_BUSY_LOW:
1151 $min_bookings = false;
1152 foreach ( $agents as $agent ) {
1153 $agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
1154 if ( $min_bookings === false && $selected_agent_id === false ) {
1155 $min_bookings = $agent_total_bookings;
1156 $selected_agent_id = $agent->id;
1157 } else {
1158 if ( $min_bookings > $agent_total_bookings ) {
1159 $min_bookings = $agent_total_bookings;
1160 $selected_agent_id = $agent->id;
1161 }
1162 }
1163 }
1164 break;
1165 }
1166 /**
1167 * Get ID of agent that will be assigned to a booking, depending on order rules, where agent is set to ANY
1168 *
1169 * @param {integer} $selected_agent_id Currently selected agent ID
1170 * @param {OsAgentModel[]} $agents Array of eligible agent models to pick from
1171 * @param {OsBookingModel} $booking Booking that needs agent ID
1172 * @param {string} $agent_order_rule Rule of agent ordering
1173 *
1174 * @returns {integer} ID of the agent that will be assigned to this booking
1175 * @since 4.7.6
1176 * @hook latepoint_get_any_agent_id_for_booking_by_rule
1177 *
1178 */
1179 $selected_agent_id = apply_filters( 'latepoint_get_any_agent_id_for_booking_by_rule', $selected_agent_id, $agents, $booking, $agent_order_rule );
1180 $booking->agent_id = $selected_agent_id;
1181
1182 return $selected_agent_id;
1183 }
1184
1185
1186 public static function get_total_bookings_for_date( $date, $conditions = [], $grouped = false ) {
1187 $args = [ 'start_date' => $date ];
1188 if ( isset( $conditions['agent_id'] ) && $conditions['agent_id'] ) {
1189 $args['agent_id'] = $conditions['agent_id'];
1190 }
1191 if ( isset( $conditions['service_id'] ) && $conditions['service_id'] ) {
1192 $args['service_id'] = $conditions['service_id'];
1193 }
1194 if ( isset( $conditions['location_id'] ) && $conditions['location_id'] ) {
1195 $args['location_id'] = $conditions['location_id'];
1196 }
1197
1198
1199 $bookings = new OsBookingModel();
1200 if ( $grouped ) {
1201 $bookings->group_by( 'start_date, start_time, end_time, service_id, location_id' );
1202 }
1203 $bookings = $bookings->where( $args );
1204
1205 return $bookings->count();
1206 }
1207
1208
1209 /**
1210 *
1211 * Get list of statuses that block timeslot availability
1212 *
1213 * @return array
1214 */
1215 public static function get_timeslot_blocking_statuses(): array {
1216 $statuses = explode( ',', OsSettingsHelper::get_settings_value( 'timeslot_blocking_statuses', '' ) );
1217
1218 /**
1219 * Get list of statuses that block timeslot availability
1220 *
1221 * @param {array} $statuses array of status codes that block timeslot availability
1222 * @returns {array} The filtered array of status codes
1223 *
1224 * @since 4.7.0
1225 * @hook latepoint_get_timeslot_blocking_statuses
1226 *
1227 */
1228 return apply_filters( 'latepoint_get_timeslot_blocking_statuses', $statuses );
1229 }
1230
1231
1232 /**
1233 *
1234 * Get list of statuses that appear on pending page
1235 *
1236 * @return array
1237 */
1238 public static function get_booking_statuses_for_pending_page(): array {
1239 $statuses = explode( ',', OsSettingsHelper::get_settings_value( 'need_action_statuses', '' ) );
1240
1241 /**
1242 * Get list of statuses that appear on pending page
1243 *
1244 * @param {array} $statuses array of status codes that appear on pending page
1245 * @returns {array} The filtered array of status codes
1246 *
1247 * @since 4.7.0
1248 * @hook latepoint_get_booking_statuses_for_pending_page
1249 *
1250 */
1251 return apply_filters( 'latepoint_get_booking_statuses_for_pending_page', $statuses );
1252 }
1253
1254 /**
1255 *
1256 * Get list of statuses that are not cancelled
1257 *
1258 * @return array
1259 */
1260 public static function get_non_cancelled_booking_statuses(): array {
1261 $statuses = self::get_statuses_list();
1262 if ( isset( $statuses[ LATEPOINT_BOOKING_STATUS_CANCELLED ] ) ) {
1263 unset( $statuses[ LATEPOINT_BOOKING_STATUS_CANCELLED ] );
1264 }
1265 $statuses = array_keys( $statuses );
1266
1267 /**
1268 * Get list of statuses that are not cancelled
1269 *
1270 * @param {array} $statuses array of status codes that are not cancelled
1271 * @returns {array} The filtered array of status codes
1272 *
1273 * @since 5.0.5
1274 * @hook get_non_cancelled_booking_statuses
1275 *
1276 */
1277 return apply_filters( 'get_non_cancelled_booking_statuses', $statuses );
1278 }
1279
1280
1281 public static function get_default_booking_status( $service_id = false ) {
1282 if ( $service_id ) {
1283 $service = new OsServiceModel( $service_id );
1284 if ( $service && ! empty( $service->id ) ) {
1285 return $service->get_default_booking_status();
1286 }
1287 }
1288 $default_status = OsSettingsHelper::get_settings_value( 'default_booking_status' );
1289 if ( $default_status ) {
1290 return $default_status;
1291 } else {
1292 return LATEPOINT_BOOKING_STATUS_APPROVED;
1293 }
1294 }
1295
1296
1297 public static function change_booking_status( $booking_id, $new_status ) {
1298 $booking = new OsBookingModel( $booking_id );
1299 if ( ! $booking_id || ! $booking ) {
1300 return false;
1301 }
1302
1303 if ( $new_status == $booking->status ) {
1304 return true;
1305 } else {
1306 $old_booking = clone $booking;
1307 if ( $booking->update_status( $new_status ) ) {
1308 do_action( 'latepoint_booking_updated', $booking, $old_booking );
1309
1310 return true;
1311 } else {
1312 return false;
1313 }
1314 }
1315 }
1316
1317
1318 /**
1319 * @param \LatePoint\Misc\BookingRequest $booking_request
1320 * @param \LatePoint\Misc\BookedPeriod[]
1321 * @param int $capacity
1322 *
1323 * @return bool
1324 */
1325 public static function is_timeframe_in_booked_periods( \LatePoint\Misc\BookingRequest $booking_request, array $booked_periods, OsServiceModel $service ): bool {
1326 if ( empty( $booked_periods ) ) {
1327 return false;
1328 }
1329 $count_existing_attendees = 0;
1330 foreach ( $booked_periods as $period ) {
1331 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() ) ) {
1332 // if it's the same service overlapping - count how many times
1333 // TODO maybe add an option to toggle on/off ability to share a timeslot capacity between two different services
1334 if ( $booking_request->service_id == $period->service_id ) {
1335 $count_existing_attendees += $period->total_attendees;
1336 } else {
1337 return true;
1338 }
1339 }
1340 }
1341 if ( $count_existing_attendees > 0 ) {
1342 // 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
1343 if ( ( $count_existing_attendees + $booking_request->total_attendees ) <= $service->get_capacity_needed_before_slot_is_blocked() ) {
1344 return false;
1345 } else {
1346 return true;
1347 }
1348 } else {
1349 // no attendees in the overlapping booked periods yet, just check if the requested number of attendees is within the service capacity
1350 if ( $booking_request->total_attendees <= $service->capacity_max ) {
1351 return false;
1352 } else {
1353 return true;
1354 }
1355 }
1356 }
1357
1358
1359 public static function is_period_overlapping( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
1360 // https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap/
1361 return ( ( $period_one_start < $period_two_end ) && ( $period_two_start < $period_one_end ) );
1362 }
1363
1364 public static function is_period_inside_another( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
1365 return ( ( $period_one_start >= $period_two_start ) && ( $period_one_end <= $period_two_end ) );
1366 }
1367
1368 // args = [agent_id, 'service_id', 'location_id']
1369 public static function get_bookings_for_date( $date, $args = [] ) {
1370 $bookings = new OsBookingModel();
1371 $args['start_date'] = $date;
1372 // if any of these are false or 0 - remove it from arguments list
1373 if ( isset( $args['location_id'] ) && empty( $args['location_id'] ) ) {
1374 unset( $args['location_id'] );
1375 }
1376 if ( isset( $args['agent_id'] ) && empty( $args['agent_id'] ) ) {
1377 unset( $args['agent_id'] );
1378 }
1379 if ( isset( $args['service_id'] ) && empty( $args['service_id'] ) ) {
1380 unset( $args['service_id'] );
1381 }
1382
1383 $bookings->where( $args )->order_by( 'start_time asc, end_time asc, service_id asc' );
1384
1385 return $bookings->get_results_as_models();
1386 }
1387
1388 /**
1389 * @param \LatePoint\Misc\Filter $filter
1390 *
1391 * @return int
1392 */
1393 public static function count_bookings( \LatePoint\Misc\Filter $filter ) {
1394 $bookings = new OsBookingModel();
1395 $query_args = [];
1396 if ( $filter->date_from ) {
1397 $query_args['start_date'] = $filter->date_from;
1398 }
1399 if ( $filter->location_id ) {
1400 $query_args['location_id'] = $filter->location_id;
1401 }
1402 if ( $filter->agent_id ) {
1403 $query_args['agent_id'] = $filter->agent_id;
1404 }
1405 if ( $filter->service_id ) {
1406 $query_args['service_id'] = $filter->service_id;
1407 }
1408
1409 return $bookings->should_not_be_cancelled()->where( $query_args )->count();
1410 }
1411
1412
1413 public static function get_nice_status_name( $status ) {
1414 $statuses_list = OsBookingHelper::get_statuses_list();
1415 if ( $status && isset( $statuses_list[ $status ] ) ) {
1416 return $statuses_list[ $status ];
1417 } else {
1418 return __( 'Undefined Status', 'latepoint' );
1419 }
1420 }
1421
1422
1423 public static function get_statuses_list() {
1424 $statuses = [
1425 LATEPOINT_BOOKING_STATUS_APPROVED => __( 'Approved', 'latepoint' ),
1426 LATEPOINT_BOOKING_STATUS_PENDING => __( 'Pending Approval', 'latepoint' ),
1427 LATEPOINT_BOOKING_STATUS_CANCELLED => __( 'Cancelled', 'latepoint' ),
1428 LATEPOINT_BOOKING_STATUS_NO_SHOW => __( 'No Show', 'latepoint' ),
1429 LATEPOINT_BOOKING_STATUS_COMPLETED => __( 'Completed', 'latepoint' ),
1430 ];
1431 $additional_statuses = array_map( 'trim', explode( ',', OsSettingsHelper::get_settings_value( 'additional_booking_statuses', '' ) ) );
1432 if ( ! empty( $additional_statuses ) ) {
1433 foreach ( $additional_statuses as $status ) {
1434 if ( ! empty( $status ) ) {
1435 $statuses[ str_replace( ' ', '_', strtolower( $status ) ) ] = $status;
1436 }
1437 }
1438 }
1439 $statuses = apply_filters( 'latepoint_booking_statuses', $statuses );
1440
1441 return $statuses;
1442 }
1443
1444
1445 public static function get_weekdays_arr( $full_name = false ) {
1446 if ( $full_name ) {
1447 $weekdays = array(
1448 __( 'Monday', 'latepoint' ),
1449 __( 'Tuesday', 'latepoint' ),
1450 __( 'Wednesday', 'latepoint' ),
1451 __( 'Thursday', 'latepoint' ),
1452 __( 'Friday', 'latepoint' ),
1453 __( 'Saturday', 'latepoint' ),
1454 __( 'Sunday', 'latepoint' ),
1455 );
1456 } else {
1457 $weekdays = array(
1458 __( 'Mon', 'latepoint' ),
1459 __( 'Tue', 'latepoint' ),
1460 __( 'Wed', 'latepoint' ),
1461 __( 'Thu', 'latepoint' ),
1462 __( 'Fri', 'latepoint' ),
1463 __( 'Sat', 'latepoint' ),
1464 __( 'Sun', 'latepoint' ),
1465 );
1466 }
1467
1468 return $weekdays;
1469 }
1470
1471 public static function get_weekday_name_by_number( $weekday_number, $full_name = false ) {
1472 $weekdays = OsBookingHelper::get_weekdays_arr( $full_name );
1473 if ( ! isset( $weekday_number ) || $weekday_number < 1 || $weekday_number > 7 ) {
1474 return '';
1475 } else {
1476 return $weekdays[ $weekday_number - 1 ];
1477 }
1478 }
1479
1480 public static function get_stat( $stat, $args = [] ) {
1481 if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
1482 return false;
1483 }
1484 $defaults = [
1485 'customer_id' => false,
1486 'agent_id' => false,
1487 'service_id' => false,
1488 'location_id' => false,
1489 'date_from' => false,
1490 'date_to' => false,
1491 'group_by' => false,
1492 'exclude_status' => false,
1493 ];
1494 $args = array_merge( $defaults, $args );
1495 $bookings = new OsBookingModel();
1496 $query_args = array( $args['date_from'], $args['date_to'] );
1497 switch ( $stat ) {
1498 case 'duration':
1499 $stat_query = 'SUM(end_time - start_time)';
1500 break;
1501 case 'price':
1502 $stat_query = 'sum(total)';
1503 break;
1504 case 'bookings':
1505 $stat_query = 'count(id)';
1506 break;
1507 }
1508 $select_query = $stat_query . ' as stat';
1509 if ( $args['group_by'] ) {
1510 $select_query .= ',' . $args['group_by'];
1511 }
1512 $bookings->select( $select_query );
1513
1514
1515 if ( $args['date_from'] ) {
1516 $bookings->where( [ 'start_date >=' => $args['date_from'] ] );
1517 }
1518 if ( $args['date_to'] ) {
1519 $bookings->where( [ 'start_date <=' => $args['date_to'] ] );
1520 }
1521 if ( $args['service_id'] ) {
1522 $bookings->where( [ 'service_id' => $args['service_id'] ] );
1523 }
1524 if ( $args['agent_id'] ) {
1525 $bookings->where( [ 'agent_id' => $args['agent_id'] ] );
1526 }
1527 if ( $args['location_id'] ) {
1528 $bookings->where( [ 'location_id' => $args['location_id'] ] );
1529 }
1530 if ( $args['customer_id'] ) {
1531 $bookings->where( [ 'customer_id' => $args['customer_id'] ] );
1532 }
1533 if ( $args['group_by'] ) {
1534 $bookings->group_by( $args['group_by'] );
1535 }
1536 // TODO, need to support custom status exclusions
1537 if ( $args['exclude_status'] == LATEPOINT_BOOKING_STATUS_CANCELLED ) {
1538 $bookings->should_not_be_cancelled();
1539 }
1540
1541 $stat_total = $bookings->get_results( ARRAY_A );
1542 if ( $args['group_by'] ) {
1543 return $stat_total;
1544 } else {
1545 return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
1546 }
1547 }
1548
1549 public static function get_new_customer_stat_for_period( DateTime $date_from, DateTime $date_to, \LatePoint\Misc\Filter $filter ) {
1550 // TODO make sure filter is respected
1551 $customers = new OsCustomerModel();
1552
1553 return $customers->filter_allowed_records()->where(
1554 [
1555 'created_at >=' => $date_from->format( 'Y-m-d' ),
1556 'created_at <=' => $date_to->format( 'Y-m-d' ),
1557 ]
1558 )->count();
1559 }
1560
1561 public static function get_stat_for_period( $stat, $date_from, $date_to, \LatePoint\Misc\Filter $filter, $group_by = false ) {
1562 if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
1563 return false;
1564 }
1565 if ( ! in_array( $group_by, [ false, 'agent_id', 'service_id', 'location_id' ] ) ) {
1566 return false;
1567 }
1568 $bookings = new OsBookingModel();
1569 switch ( $stat ) {
1570 case 'duration':
1571 $stat_query = 'SUM(end_time - start_time)';
1572 break;
1573 case 'price':
1574 $stat_query = 'sum(' . LATEPOINT_TABLE_ORDER_ITEMS . '.total)';
1575 $bookings->join( LATEPOINT_TABLE_ORDER_ITEMS, [ 'id' => $bookings->table_name . '.order_item_id' ] );
1576 $bookings->join( LATEPOINT_TABLE_ORDERS, [ 'id' => LATEPOINT_TABLE_ORDER_ITEMS . '.order_id' ] );
1577 break;
1578 case 'bookings':
1579 $stat_query = 'count(id)';
1580 break;
1581 }
1582 $select_query = $stat_query . ' as stat';
1583 if ( $group_by ) {
1584 $select_query .= ',' . $group_by;
1585 }
1586 $bookings->select( $select_query )->where(
1587 [
1588 'start_date >=' => $date_from,
1589 'start_date <= ' => $date_to,
1590 ]
1591 );
1592
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
1603 $bookings->should_not_be_cancelled();
1604
1605 if ( $group_by ) {
1606 $bookings->group_by( $group_by );
1607 }
1608
1609 $stat_total = $bookings->get_results( ARRAY_A );
1610 if ( $group_by ) {
1611 return $stat_total;
1612 } else {
1613 return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
1614 }
1615 }
1616
1617 public static function get_total_bookings_per_day_for_period( $date_from, $date_to, \LatePoint\Misc\Filter $filter ) {
1618 $bookings = new OsBookingModel();
1619 $bookings->select( 'count(id) as bookings_per_day, start_date' )
1620 ->where(
1621 [
1622 'start_date >=' => $date_from,
1623 'start_date <=' => $date_to,
1624 ]
1625 )
1626 ->where( [ 'status NOT IN' => OsCalendarHelper::get_booking_statuses_hidden_from_calendar() ] );
1627 if ( $filter->service_id ) {
1628 $bookings->where( [ 'service_id' => $filter->service_id ] );
1629 }
1630 if ( $filter->agent_id ) {
1631 $bookings->where( [ 'agent_id' => $filter->agent_id ] );
1632 }
1633 if ( $filter->location_id ) {
1634 $bookings->where( [ 'location_id' => $filter->location_id ] );
1635 }
1636 $bookings->group_by( 'start_date' );
1637
1638 return $bookings->get_results();
1639 }
1640
1641
1642 public static function get_min_max_work_periods( $specific_weekdays = false, $service_id = false, $agent_id = false ) {
1643 $select_string = 'MIN(start_time) as start_time, MAX(end_time) as end_time';
1644 $work_periods = new OsWorkPeriodModel();
1645 $work_periods = $work_periods->select( $select_string );
1646 $query_args = array(
1647 'service_id' => 0,
1648 'agent_id' => 0,
1649 );
1650 if ( $service_id ) {
1651 $query_args['service_id'] = $service_id;
1652 }
1653 if ( $agent_id ) {
1654 $query_args['agent_id'] = $agent_id;
1655 }
1656 if ( $specific_weekdays && ! empty( $specific_weekdays ) ) {
1657 $query_args['week_day'] = $specific_weekdays;
1658 }
1659 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1660 if ( ( $service_id || $agent_id ) && empty( $results['min_start_time'] ) ) {
1661 if ( $service_id && empty( $results['min_start_time'] ) ) {
1662 $query_args['service_id'] = 0;
1663 $work_periods = new OsWorkPeriodModel();
1664 $work_periods = $work_periods->select( $select_string );
1665 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1666 }
1667 if ( $agent_id && empty( $results['min_start_time'] ) ) {
1668 $query_args['agent_id'] = 0;
1669 $work_periods = new OsWorkPeriodModel();
1670 $work_periods = $work_periods->select( $select_string );
1671 $results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
1672 }
1673 }
1674 if ( $results ) {
1675 return array( $results['start_time'], $results['end_time'] );
1676 } else {
1677 return false;
1678 }
1679 }
1680
1681
1682 public static function get_work_start_end_time_for_multiple_dates( $dates = false, $service_id = false, $agent_id = false ) {
1683 $specific_weekdays = array();
1684 if ( $dates ) {
1685 foreach ( $dates as $date ) {
1686 $target_date = new OsWpDateTime( $date );
1687 $weekday = $target_date->format( 'N' );
1688 if ( ! in_array( $weekday, $specific_weekdays ) ) {
1689 $specific_weekdays[] = $weekday;
1690 }
1691 }
1692 }
1693 $work_minmax_start_end = self::get_min_max_work_periods( $specific_weekdays, $service_id, $agent_id );
1694
1695 return $work_minmax_start_end;
1696 }
1697
1698 /**
1699 * @param int $minute
1700 * @param \LatePoint\Misc\WorkPeriod[] $work_periods_arr
1701 *
1702 * @return bool
1703 */
1704 public static function is_minute_in_work_periods( int $minute, array $work_periods_arr ): bool {
1705 // print_r($work_periods_arr);
1706 if ( empty( $work_periods_arr ) ) {
1707 return false;
1708 }
1709 foreach ( $work_periods_arr as $work_period ) {
1710 // end of period does not count because we cant make appointment with 0 duration
1711 if ( $work_period->start_time <= $minute && $work_period->end_time > $minute ) {
1712 return true;
1713 }
1714 }
1715
1716 return false;
1717 }
1718
1719 public static function get_calendar_start_end_time( $bookings, $work_start_minutes, $work_end_minutes ) {
1720 $calendar_start_minutes = $work_start_minutes;
1721 $calendar_end_minutes = $work_end_minutes;
1722 if ( $bookings ) {
1723 foreach ( $bookings as $bookings_for_agent ) {
1724 if ( $bookings_for_agent ) {
1725 foreach ( $bookings_for_agent as $booking ) {
1726 if ( $booking->start_time < $calendar_start_minutes ) {
1727 $calendar_start_minutes = $booking->start_time;
1728 }
1729 if ( $booking->end_time > $calendar_end_minutes ) {
1730 $calendar_end_minutes = $booking->end_time;
1731 }
1732 }
1733 }
1734 }
1735 }
1736
1737 return [ $calendar_start_minutes, $calendar_end_minutes ];
1738 }
1739
1740 public static function generate_direct_manage_booking_url( OsBookingModel $booking, string $for ): string {
1741 if ( ! in_array( $for, [ 'agent', 'customer' ] ) ) {
1742 return '';
1743 }
1744 $key = $booking->get_key_to_manage_for( $for );
1745 $url = OsRouterHelper::build_admin_post_link( [ 'manage_booking_by_key', 'show' ], [ 'key' => $key ] );
1746
1747 return $url;
1748 }
1749
1750 /**
1751 * Renders the customer-facing "Cancel" button for a booking, behind a filter so addons
1752 * (e.g. the pro cancellation-reason feature) can replace its behavior without editing core.
1753 *
1754 * @param OsBookingModel $booking
1755 * @param string $route_prefix controller prefix for the cancel route ('customer_cabinet' or 'manage_booking_by_key')
1756 * @param string|null $key manage-by-key key (null for the logged-in customer cabinet)
1757 * @param string $css_classes button CSS classes
1758 */
1759 public static function generate_cancel_booking_button( OsBookingModel $booking, string $route_prefix, ?string $key = null, string $css_classes = 'latepoint-btn latepoint-btn-danger latepoint-btn-link' ): string {
1760 $os_params = OsUtilHelper::build_os_params( $key ? [ 'key' => $key ] : [ 'id' => $booking->id ], 'cancel_booking_' . $booking->id );
1761 $route = OsRouterHelper::build_route_name( $route_prefix, 'request_cancellation' );
1762 ob_start();
1763 ?>
1764 <a href="#" class="<?php echo esc_attr( $css_classes ); ?> os-confirm-alert"
1765 data-os-confirm-title="<?php esc_attr_e( 'Cancel Appointment?', 'latepoint' ); ?>"
1766 data-os-prompt="<?php esc_attr_e( 'Are you sure you want to cancel this appointment?', 'latepoint' ); ?>"
1767 data-os-confirm-button="<?php esc_attr_e( 'Confirm Cancellation', 'latepoint' ); ?>"
1768 data-os-success-action="reload"
1769 data-os-action="<?php echo esc_attr( $route ); ?>"
1770 data-os-params="<?php echo esc_attr( $os_params ); ?>">
1771 <i class="latepoint-icon latepoint-icon-ui-24"></i>
1772 <span><?php esc_html_e( 'Cancel', 'latepoint' ); ?></span>
1773 </a>
1774 <?php
1775 $html = ob_get_clean();
1776
1777 /**
1778 * Filters the customer-facing cancel-booking button HTML.
1779 *
1780 * @param string $html
1781 * @param OsBookingModel $booking
1782 * @param string $route_prefix
1783 * @param string|null $key
1784 *
1785 * @since 5.2.0
1786 * @hook latepoint_customer_cancel_booking_button
1787 */
1788 return apply_filters( 'latepoint_customer_cancel_booking_button', $html, $booking, $route_prefix, $key );
1789 }
1790
1791 public static function generate_summary_actions_for_booking( OsBookingModel $booking, ?string $key = null ) {
1792 ?>
1793 <div class="booking-full-summary-actions">
1794 <div class="add-to-calendar-wrapper">
1795 <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>
1796 <?php echo OsBookingHelper::generate_add_to_calendar_links( $booking, $key ?? $booking->get_key_to_manage_for( 'customer' ) ); ?>
1797 </div>
1798 <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>
1799 <?php
1800 if ( $booking->is_upcoming() ) {
1801 if ( OsCustomerHelper::can_reschedule_booking( $booking ) ) { ?>
1802 <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">
1803 <i class="latepoint-icon latepoint-icon-calendar"></i>
1804 <span><?php esc_html_e( 'Reschedule', 'latepoint' ); ?></span>
1805 </a>
1806 <?php
1807 }
1808 if ( OsCustomerHelper::can_cancel_booking( $booking ) ) {
1809 echo self::generate_cancel_booking_button( $booking, 'manage_booking_by_key', $key ?? $booking->get_key_to_manage_for( 'customer' ), 'booking-summary-action-btn cancel-appointment-btn' );
1810 }
1811 }
1812 do_action( 'latepoint_booking_summary_after_booking_actions', $booking );
1813 ?>
1814 </div>
1815 <?php
1816 }
1817
1818 public static function generate_summary_for_booking( OsBookingModel $booking, $cart_item_id = false, ?string $viewer = 'customer' ): string {
1819 $summary_html = '';
1820 $summary_html .= apply_filters( 'latepoint_booking_summary_before_summary_box', '', $booking );
1821 $summary_html .= '<div class="summary-box main-box" ' . ( ( $cart_item_id ) ? 'data-cart-item-id="' . $cart_item_id . '"' : '' ) . '>';
1822 $output_timezone_name = $viewer == 'customer' ? $booking->get_customer_timezone_name() : OsTimeHelper::get_wp_timezone_name();
1823 if ( ! empty( $booking->start_datetime_utc ) ) {
1824 $summary_html .= '<div class="summary-box-booking-date-box">';
1825 $summary_html .= '<div class="summary-box-booking-date-day">' . $booking->start_datetime_in_format( 'j', $output_timezone_name ) . '</div>';
1826 $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>';
1827 $summary_html .= '</div>';
1828 }
1829 $summary_html .= '<div class="summary-box-inner">';
1830 $service_headings = [];
1831 $service_headings = apply_filters( 'latepoint_booking_summary_service_headings', $service_headings, $booking );
1832 if ( $service_headings ) {
1833 $summary_html .= '<div class="summary-box-heading">';
1834 foreach ( $service_headings as $heading ) {
1835 $summary_html .= '<div class="sbh-item">' . $heading . '</div>';
1836 }
1837 $summary_html .= '<div class="sbh-line"></div>';
1838 $summary_html .= '</div>';
1839 }
1840 $summary_html .= '<div class="summary-box-content os-cart-item">';
1841 if ( $cart_item_id && OsCartsHelper::can_checkout_multiple_items() ) {
1842 $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-nonce="' . esc_attr( wp_create_nonce( 'remove_item_from_cart' ) ) . '" data-route="' . OsRouterHelper::build_route_name( 'carts', 'remove_item_from_cart' ) . '">
1843 <div class="os-remove-from-cart-icon"></div>
1844 </div>';
1845 }
1846 $summary_html .= '<div class="sbc-big-item">' . $booking->get_service_name_for_summary() . '</div>';
1847 if ( $booking->start_date ) {
1848 $summary_html .= '<div class="sbc-highlighted-item">' . $booking->get_nice_datetime_for_summary( $viewer ) . '</div>';
1849 }
1850 /**
1851 * Output summary of the booking data after a start date and time
1852 *
1853 * @since 5.2.0
1854 * @hook latepoint_summary_booking_info_after_start_date
1855 *
1856 * @param {string} $summary_html HTML of the summary
1857 * @param {OsBookingModel} $booking Booking object that is being outputted
1858 * @param {string} $cart_item_id ID of a cart item this booking belongs to
1859 * @param {string} $viewer determines who is viewing this summary, can be customer or agent
1860 *
1861 * @returns {string} Filtered HTML
1862 */
1863 $summary_html = apply_filters( 'latepoint_summary_booking_info_after_start_date', $summary_html, $booking, $cart_item_id, $viewer );
1864 $summary_html .= '</div>';
1865
1866 $service_attributes = [];
1867 $service_attributes = apply_filters( 'latepoint_booking_summary_service_attributes', $service_attributes, $booking );
1868 if ( $service_attributes ) {
1869 $summary_html .= '<div class="summary-attributes sa-clean">';
1870 foreach ( $service_attributes as $attribute ) {
1871 $summary_html .= '<span>' . $attribute['label'] . ': <strong>' . $attribute['value'] . '</strong></span>';
1872 }
1873 $summary_html .= '</div>';
1874 }
1875 $summary_html .= '</div>';
1876 $summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box_inner', '', $booking );
1877 $summary_html .= '</div>';
1878 $summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box', '', $booking );
1879
1880 return $summary_html;
1881 }
1882
1883
1884 /**
1885 * @param OsBookingModel[] $bookings
1886 *
1887 * @return bool
1888 */
1889 public static function bookings_have_same_agent( array $bookings ): bool {
1890 return ( count( array_unique( array_column( $bookings, 'agent_id' ) ) ) == 1 );
1891 }
1892
1893 /**
1894 * @param OsBookingModel[] $bookings
1895 *
1896 * @return bool
1897 */
1898 public static function bookings_have_same_location( array $bookings ): bool {
1899 return ( count( array_unique( array_column( $bookings, 'location_id' ) ) ) == 1 );
1900 }
1901
1902 /**
1903 * @param OsBookingModel[] $bookings
1904 *
1905 * @return bool
1906 */
1907 public static function bookings_have_same_service( array $bookings ): bool {
1908 return ( count( array_unique( array_column( $bookings, 'service_id' ) ) ) == 1 );
1909 }
1910
1911 public static function prepare_new_from_params( array $params ): OsBookingModel {
1912 $booking = new OsBookingModel();
1913
1914 $services = OsServiceHelper::get_allowed_active_services();
1915 $agents = OsAgentHelper::get_allowed_active_agents();
1916
1917 // LOAD FROM PASSED PARAMS
1918 $booking->order_item_id = $params['order_item_id'] ?? '';
1919 $booking->service_id = ! empty( $params['service_id'] ) ? OsUtilHelper::first_value_if_array( $params['service_id'] ) : '';
1920 if ( empty( $booking->service_id ) && ! empty( $services ) ) {
1921 $booking->service_id = $services[0]->id;
1922 }
1923
1924 $booking->agent_id = ! empty( $params['agent_id'] ) ? OsUtilHelper::first_value_if_array( $params['agent_id'] ) : '';
1925 if ( empty( $booking->agent_id ) && ! empty( $agents ) ) {
1926 $booking->agent_id = $agents[0]->id;
1927 }
1928
1929 if ( ! empty( $params['order_id'] ) ) {
1930 $order = new OsOrderModel( $params['order_id'] );
1931 $booking->customer_id = $order->customer_id;
1932 } else {
1933 $booking->customer_id = ! empty( $params['customer_id'] ) ? OsUtilHelper::first_value_if_array( $params['customer_id'] ) : '';
1934 }
1935
1936 $booking->location_id = ! empty( $params['location_id'] ) ? OsUtilHelper::first_value_if_array( $params['location_id'] ) : OsLocationHelper::get_default_location_id( true );
1937 $booking->start_date = $params['start_date'] ?? OsTimeHelper::today_date( 'Y-m-d' );
1938 $booking->start_time = $params['start_time'] ?? 600;
1939
1940 $booking->end_time = ( $booking->service_id ) ? $booking->calculate_end_time() : $booking->start_time + 60;
1941 $booking->end_date = $booking->calculate_end_date();
1942 $booking->buffer_before = ( $booking->service_id ) ? $booking->service->buffer_before : 0;
1943 $booking->buffer_after = ( $booking->service_id ) ? $booking->service->buffer_after : 0;
1944 $booking->status = LATEPOINT_BOOKING_STATUS_APPROVED;
1945
1946 return $booking;
1947 }
1948
1949 /**
1950 * Returns the <th> HTML for a single column header cell in the bookings table.
1951 *
1952 * @param string $col_key Unified column key (e.g. 'id', 'datetime', 'customer.email').
1953 * @param array $col_def Column definition from get_all_bookings_table_columns().
1954 * @param string $order_key Currently active sort key.
1955 * @param string $order_dir Currently active sort direction ('asc'|'desc').
1956 * @return string
1957 */
1958 public static function render_table_header_cell( string $col_key, array $col_def, string $order_key, string $order_dir ): string {
1959 switch ( $col_key ) {
1960 case 'id':
1961 $active_class = ( $order_key === 'booking_id' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1962 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_id">' . esc_html__( 'ID', 'latepoint' ) . '</th>';
1963
1964 case 'service':
1965 return '<th>' . esc_html__( 'Service', 'latepoint' ) . '</th>';
1966
1967 case 'datetime':
1968 $active_class = ( $order_key === 'booking_start_datetime' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1969 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_start_datetime">' . esc_html__( 'Date/Time', 'latepoint' ) . '</th>';
1970
1971 case 'time_left':
1972 $active_class = ( $order_key === 'booking_time_left' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1973 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_time_left">' . esc_html__( 'Time Left', 'latepoint' ) . '</th>';
1974
1975 case 'agent':
1976 return '<th>' . esc_html__( 'Agent', 'latepoint' ) . '</th>';
1977
1978 case 'location':
1979 return '<th>' . esc_html__( 'Location', 'latepoint' ) . '</th>';
1980
1981 case 'customer':
1982 return '<th>' . esc_html__( 'Customer', 'latepoint' ) . '</th>';
1983
1984 case 'status':
1985 return '<th>' . esc_html__( 'Status', 'latepoint' ) . '</th>';
1986
1987 case 'payment_status':
1988 return '<th>' . esc_html__( 'Payment Status', 'latepoint' ) . '</th>';
1989
1990 case 'created_on':
1991 $active_class = ( $order_key === 'booking_created_on' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1992 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_created_on">' . esc_html__( 'Created On', 'latepoint' ) . '</th>';
1993
1994 default:
1995 // Extra columns.
1996 return '<th>' . esc_html( $col_def['label'] ) . '</th>';
1997 }
1998 }
1999
2000 /**
2001 * Returns the <th> HTML for a single column filter cell in the bookings table header.
2002 *
2003 * @param string $col_key Unified column key.
2004 * @param array $col_def Column definition.
2005 * @param array $services_list Keyed list of services for the service filter select.
2006 * @param array $agents_list Keyed list of agents for the agent filter select.
2007 * @param array $locations_list Keyed list of locations for the location filter select.
2008 * @return string
2009 */
2010 public static function render_table_filter_cell( string $col_key, array $col_def, array $services_list, array $agents_list, array $locations_list ): string {
2011 switch ( $col_key ) {
2012 case 'id':
2013 return '<th>' . OsFormHelper::text_field(
2014 'filter[id]',
2015 false,
2016 '',
2017 [
2018 'placeholder' => __( 'ID', 'latepoint' ),
2019 'theme' => 'bordered',
2020 'style' => 'width: 60px;',
2021 'class' => 'os-table-filter',
2022 ]
2023 ) . '</th>';
2024
2025 case 'service':
2026 return '<th>' . OsFormHelper::select_field(
2027 'filter[service_id]',
2028 false,
2029 $services_list,
2030 '',
2031 [
2032 'placeholder' => __( 'All Services', 'latepoint' ),
2033 'class' => 'os-table-filter',
2034 ]
2035 ) . '</th>';
2036
2037 case 'datetime':
2038 ob_start();
2039 ?>
2040 <th>
2041 <div class="os-form-group">
2042 <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' ); ?>">
2043 <span class="range-picker-value"><?php esc_html_e( 'Filter Date', 'latepoint' ); ?></span>
2044 <i class="latepoint-icon latepoint-icon-chevron-down"></i>
2045 <input type="hidden" class="os-table-filter os-datepicker-date-from" name="filter[booking_date_from]" value=""/>
2046 <input type="hidden" class="os-table-filter os-datepicker-date-to" name="filter[booking_date_to]" value=""/>
2047 </div>
2048 </div>
2049 </th>
2050 <?php
2051 return ob_get_clean();
2052
2053 case 'time_left':
2054 return '<th>' . OsFormHelper::select_field(
2055 'filter[time_status]',
2056 false,
2057 [
2058 'upcoming' => __( 'Upcoming', 'latepoint' ),
2059 'past' => __( 'Past', 'latepoint' ),
2060 'now' => __( 'Happening Now', 'latepoint' ),
2061 ],
2062 '',
2063 [
2064 'placeholder' => __( 'Show All', 'latepoint' ),
2065 'class' => 'os-table-filter',
2066 ]
2067 ) . '</th>';
2068
2069 case 'agent':
2070 return '<th>' . OsFormHelper::select_field(
2071 'filter[agent_id]',
2072 false,
2073 $agents_list,
2074 '',
2075 [
2076 'placeholder' => __( 'All Agents', 'latepoint' ),
2077 'class' => 'os-table-filter',
2078 ]
2079 ) . '</th>';
2080
2081 case 'location':
2082 return '<th>' . OsFormHelper::select_field(
2083 'filter[location_id]',
2084 false,
2085 $locations_list,
2086 '',
2087 [
2088 'placeholder' => __( 'All Locations', 'latepoint' ),
2089 'class' => 'os-table-filter',
2090 ]
2091 ) . '</th>';
2092
2093 case 'customer':
2094 return '<th>' . OsFormHelper::text_field(
2095 'filter[customer][full_name]',
2096 false,
2097 '',
2098 [
2099 'class' => 'os-table-filter',
2100 'theme' => 'bordered',
2101 'placeholder' => __( 'Search by Customer', 'latepoint' ),
2102 ]
2103 ) . '</th>';
2104
2105 case 'status':
2106 return '<th>' . OsFormHelper::select_field(
2107 'filter[status]',
2108 false,
2109 OsBookingHelper::get_statuses_list(),
2110 '',
2111 [
2112 'placeholder' => __( 'Show All', 'latepoint' ),
2113 'class' => 'os-table-filter',
2114 ]
2115 ) . '</th>';
2116
2117 case 'payment_status':
2118 return '<th>' . OsFormHelper::select_field(
2119 'filter[order][payment_status]',
2120 false,
2121 OsOrdersHelper::get_order_payment_statuses_list(),
2122 '',
2123 [
2124 'placeholder' => __( 'Show All', 'latepoint' ),
2125 'class' => 'os-table-filter',
2126 ]
2127 ) . '</th>';
2128
2129 case 'created_on':
2130 ob_start();
2131 ?>
2132 <th>
2133 <div class="os-form-group">
2134 <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' ); ?>">
2135 <span class="range-picker-value"><?php esc_html_e( 'Filter Date', 'latepoint' ); ?></span>
2136 <i class="latepoint-icon latepoint-icon-chevron-down"></i>
2137 <input type="hidden" class="os-table-filter os-datepicker-date-from" name="filter[created_date_from]" value=""/>
2138 <input type="hidden" class="os-table-filter os-datepicker-date-to" name="filter[created_date_to]" value=""/>
2139 </div>
2140 </div>
2141 </th>
2142 <?php
2143 return ob_get_clean();
2144
2145 default:
2146 // Extra columns.
2147 if ( 'extra' !== $col_def['type'] ) {
2148 return '<th></th>';
2149 }
2150 $extra_type = $col_def['extra_type'];
2151 $extra_key = $col_def['extra_key'];
2152 $extra_label = $col_def['label'];
2153 $field_name = ( 'booking' !== $extra_type ) ? 'filter[' . $extra_type . '][' . $extra_key . ']' : 'filter[' . $extra_key . ']';
2154 // Skip the filter input for magic properties that can't be queried.
2155 if ( 'booking' === $extra_type && ! property_exists( 'OsBookingModel', $extra_key ) ) {
2156 return '<th></th>';
2157 }
2158 return '<th>' . OsFormHelper::text_field(
2159 $field_name,
2160 false,
2161 '',
2162 [
2163 'class' => 'os-table-filter',
2164 'theme' => 'bordered',
2165 'placeholder' => $extra_label,
2166 ]
2167 ) . '</th>';
2168 }
2169 }
2170
2171 /**
2172 * Returns the <td> HTML for a single data cell in the bookings table body.
2173 *
2174 * @param string $col_key Unified column key.
2175 * @param array $col_def Column definition.
2176 * @param OsBookingModel $booking Current booking row.
2177 * @return string
2178 */
2179 public static function render_table_body_cell( string $col_key, array $col_def, OsBookingModel $booking ): string {
2180 switch ( $col_key ) {
2181 case 'id':
2182 return '<td class="os-column-faded text-right has-floating-button">' .
2183 esc_html( $booking->id ) .
2184 '<div class="os-floating-button"><i class="latepoint-icon latepoint-icon-edit-3"></i></div>' .
2185 '</td>';
2186
2187 case 'service':
2188 return '<td><div class="os-with-service-color"><span class="cell-link-content">' .
2189 '<span class="os-column-service-color" style="background-color: ' . esc_attr( $booking->service->bg_color ) . '"></span>' .
2190 '<span>' . esc_html( $booking->service->name ) . '</span>' .
2191 '</span></div></td>';
2192
2193 case 'datetime':
2194 return '<td><strong>' . esc_html( $booking->nice_start_date ) . '</strong> <span class="os-dot"></span> <span>' . esc_html( $booking->nice_start_time ) . '</span></td>';
2195
2196 case 'time_left':
2197 $time_html = '';
2198 switch ( $booking->time_status() ) {
2199 case 'upcoming':
2200 $time_html = $booking->time_left;
2201 break;
2202 case 'now':
2203 $time_html = '<span class="time-left is-now">' . esc_html__( 'Now', 'latepoint' ) . '</span>';
2204 break;
2205 case 'past':
2206 $time_html = '<span class="time-left is-past">' . esc_html__( 'Past', 'latepoint' ) . '</span>';
2207 break;
2208 }
2209 return '<td><span class="in-table-time-left">' . $time_html . '</span></td>';
2210
2211 case 'agent':
2212 return '<td><div class="os-with-avatar">' .
2213 '<span class="cell-link-content">' .
2214 '<span class="os-avatar" style="background-image: url(' . esc_url( $booking->agent->get_avatar_url() ) . ')"></span>' .
2215 '<span class="os-name">' . esc_html( $booking->agent->full_name ) . '</span>' .
2216 '</span>' .
2217 '<div class="os-clickable-popup-trigger"' .
2218 ' data-route="' . esc_attr( OsRouterHelper::build_route_name( 'agents', 'mini_profile' ) ) . '"' .
2219 ' data-os-params="' . esc_attr(
2220 OsUtilHelper::build_os_params(
2221 [
2222 'agent_id' => $booking->agent_id,
2223 'booking_id' => $booking->id,
2224 ]
2225 )
2226 ) . '">' .
2227 '<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
2228 '</div>' .
2229 '</div></td>';
2230
2231 case 'location':
2232 return '<td>' . esc_html( $booking->location->name ) . '</td>';
2233
2234 case 'customer':
2235 return '<td><div class="os-with-avatar">' .
2236 '<span class="cell-link-content">' .
2237 '<span class="os-avatar" style="background-image: url(' . esc_url( $booking->customer->get_avatar_url() ) . ')"></span>' .
2238 '<span class="os-name">' . esc_html( $booking->customer->full_name ) . '</span>' .
2239 '</span>' .
2240 '<div class="os-clickable-popup-trigger"' .
2241 ' data-route="' . esc_attr( OsRouterHelper::build_route_name( 'customers', 'mini_profile' ) ) . '"' .
2242 ' data-os-params="' . esc_attr(
2243 OsUtilHelper::build_os_params(
2244 [
2245 'customer_id' => $booking->customer_id,
2246 'booking_id' => $booking->id,
2247 ]
2248 )
2249 ) . '">' .
2250 '<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
2251 '</div>' .
2252 '</div></td>';
2253
2254 case 'status':
2255 return '<td><span class="os-column-status os-column-status-' . esc_attr( $booking->status ) . '">' . esc_html( $booking->nice_status ) . '</span></td>';
2256
2257 case 'payment_status':
2258 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>';
2259
2260 case 'created_on':
2261 return '<td>' . esc_html( $booking->nice_created_at ) . '</td>';
2262
2263 default:
2264 // Extra columns: read from the appropriate model via property, getter, or meta.
2265 if ( 'extra' !== $col_def['type'] ) {
2266 return '<td></td>';
2267 }
2268 $extra_type = $col_def['extra_type'];
2269 $extra_key = $col_def['extra_key'];
2270 $model_obj = ( 'booking' === $extra_type ) ? $booking : $booking->$extra_type;
2271
2272 if ( property_exists( $model_obj, $extra_key ) || method_exists( $model_obj, 'get_' . $extra_key ) ) {
2273 return '<td>' . esc_html( $model_obj->$extra_key ) . '</td>';
2274 }
2275
2276 $column_value = $model_obj->get_meta_by_key( $extra_key );
2277 if ( $column_value &&
2278 ( $custom_fields_raw = OsSettingsHelper::get_settings_value( 'custom_fields_for_' . $extra_type, false ) ) &&
2279 ( $custom_fields_arr = json_decode( $custom_fields_raw, true ) ) &&
2280 isset( $custom_fields_arr[ $extra_key ]['type'] ) &&
2281 'multiselect' === $custom_fields_arr[ $extra_key ]['type']
2282 ) {
2283 $decoded = json_decode( $column_value );
2284 $column_value = is_array( $decoded ) ? implode( ', ', $decoded ) : $column_value;
2285 }
2286 return '<td>' . esc_html( $column_value ) . '</td>';
2287 }
2288 }
2289
2290 /**
2291 * Returns the <th> HTML for the bulk-selection column header (with the select-all checkbox).
2292 *
2293 * @return string
2294 */
2295 public static function render_bulk_select_header_cell(): string {
2296 return '<th class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
2297 '<label class="os-bulk-row-check-w">' .
2298 '<input type="checkbox" class="os-bulk-select-all" aria-label="' . esc_attr__( 'Select all appointments on this page', 'latepoint' ) . '" />' .
2299 '<span class="os-bulk-check-box"></span>' .
2300 '</label>' .
2301 '</th>';
2302 }
2303
2304 /**
2305 * Returns the empty spacer <th> HTML for the bulk-selection column in filter and footer rows.
2306 *
2307 * @return string
2308 */
2309 public static function render_bulk_select_spacer_cell(): string {
2310 return '<th class="os-bulk-select-cell"></th>';
2311 }
2312
2313 /**
2314 * Returns the <td> HTML for the bulk-selection column on a single body row.
2315 *
2316 * @param OsBookingModel $booking Current booking row.
2317 * @return string
2318 */
2319 public static function render_bulk_select_body_cell( OsBookingModel $booking ): string {
2320 /* translators: %d: appointment ID */
2321 $aria_label = sprintf( __( 'Select appointment %d', 'latepoint' ), $booking->id );
2322 return '<td class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
2323 '<label class="os-bulk-row-check-w">' .
2324 '<input type="checkbox" class="os-bulk-row-check" value="' . esc_attr( $booking->id ) . '" aria-label="' . esc_attr( $aria_label ) . '" />' .
2325 '<span class="os-bulk-check-box"></span>' .
2326 '</label>' .
2327 '</td>';
2328 }
2329
2330 /**
2331 * Returns the HTML for the bulk-actions toolbar shown above the appointments table.
2332 *
2333 * @return string
2334 */
2335 public static function render_bulk_actions_bar(): string {
2336 return '<div class="os-bulk-actions-bar" data-bulk-nonce="' . esc_attr( wp_create_nonce( 'bulk_destroy_bookings' ) ) . '">' .
2337 '<div class="os-bulk-actions-info">' .
2338 '<span class="os-bulk-selected-count">0</span>' .
2339 // Label is swapped between singular and plural by JS based on selection count.
2340 '<span class="os-bulk-selected-label">' . esc_html__( 'Appointments selected', 'latepoint' ) . '</span>' .
2341 '</div>' .
2342 '<div class="os-bulk-actions-controls">' .
2343 '<a href="#" class="latepoint-btn latepoint-btn-danger os-bulk-action-delete">' .
2344 '<i class="latepoint-icon latepoint-icon-trash-2"></i><span>' . esc_html__( 'Delete Selected', 'latepoint' ) . '</span>' .
2345 '</a>' .
2346 '<a href="#" class="os-bulk-actions-clear">' . esc_html__( 'Clear selection', 'latepoint' ) . '</a>' .
2347 '</div>' .
2348 '</div>';
2349 }
2350 }
2351