PluginProbe ʕ •ᴥ•ʔ
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress / 5.6.3
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress v5.6.3
5.6.8 5.6.7 5.6.6 5.6.5 5.6.4 5.6.3 5.6.2 5.6.1 5.6.0 5.5.2 5.5.1 5.5.0 5.4.2 trunk 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.1.91 5.1.92 5.1.93 5.1.94 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1
latepoint / lib / helpers / booking_helper.php
latepoint / lib / helpers Last commit date
activities_helper.php 4 months ago agent_helper.php 4 months ago analytics_helper.php 2 weeks ago auth_helper.php 2 months ago blocks_helper.php 4 weeks ago booking_helper.php 2 weeks ago bricks_helper.php 4 months ago bundles_helper.php 3 months ago calendar_helper.php 4 weeks ago carts_helper.php 4 months ago connector_helper.php 4 months ago csv_helper.php 4 months ago customer_helper.php 2 months ago customer_import_helper.php 1 month ago database_helper.php 2 weeks ago debug_helper.php 4 months ago defaults_helper.php 4 months ago elementor_helper.php 4 months ago email_helper.php 4 months ago encrypt_helper.php 4 months ago events_helper.php 3 months ago form_helper.php 4 months ago icalendar_helper.php 4 months ago image_helper.php 4 months ago invoices_helper.php 4 weeks ago license_helper.php 4 months ago location_helper.php 4 months ago marketing_systems_helper.php 4 months ago meeting_systems_helper.php 4 months ago menu_helper.php 4 weeks ago meta_helper.php 4 months ago migrations_helper.php 4 months ago money_helper.php 3 months ago notifications_helper.php 4 months ago nps_survey_helper.php 4 months ago order_intent_helper.php 2 months ago orders_helper.php 3 months ago otp_helper.php 3 months ago pages_helper.php 4 months ago params_helper.php 4 months ago payments_helper.php 2 months ago plugin_version_update_helper.php 2 months ago price_breakdown_helper.php 4 months ago process_jobs_helper.php 4 months ago processes_helper.php 4 months ago razorpay_connect_helper.php 2 weeks ago replacer_helper.php 4 months ago resource_helper.php 4 months ago roles_helper.php 4 weeks ago router_helper.php 4 months ago service_helper.php 4 months ago sessions_helper.php 4 months ago settings_helper.php 2 weeks ago short_links_systems_helper.php 4 months ago shortcodes_helper.php 4 weeks ago sms_helper.php 4 months ago steps_helper.php 2 weeks ago stripe_connect_helper.php 2 weeks ago styles_helper.php 4 months ago support_topics_helper.php 4 months ago time_helper.php 3 months ago timeline_helper.php 2 months ago transaction_helper.php 1 month ago transaction_intent_helper.php 4 months ago util_helper.php 1 month ago version_specific_updates_helper.php 4 months ago whatsapp_helper.php 1 month ago work_periods_helper.php 3 months ago wp_datetime.php 4 months ago wp_user_helper.php 4 months ago
booking_helper.php
2318 lines
1 <?php
2
3 class OsBookingHelper {
4
5
6 /**
7 * @param OsBookingModel $booking
8 *
9 * @return mixed|void
10 *
11 * Returns full amount to charge in database format 1999.0000
12 *
13 */
14 public static function calculate_full_amount_for_booking( OsBookingModel $booking ) {
15 if ( ! $booking->service_id ) {
16 return 0;
17 }
18 $amount = self::calculate_full_amount_for_service( $booking );
19 $amount = apply_filters( 'latepoint_calculate_full_amount_for_booking', $amount, $booking );
20 $amount = OsMoneyHelper::pad_to_db_format( $amount );
21
22 return $amount;
23 }
24
25
26 /**
27 * @param OsBookingModel $booking
28 *
29 * @return mixed|void
30 *
31 */
32 public static function calculate_full_amount_for_service( OsBookingModel $booking ) {
33 if ( ! $booking->service_id ) {
34 return 0;
35 }
36 $service = new OsServiceModel( $booking->service_id );
37 $amount_for_service = $service->get_full_amount_for_duration( $booking->duration );
38 $amount_for_service = apply_filters( 'latepoint_full_amount_for_service', $amount_for_service, $booking );
39
40 return $amount_for_service;
41 }
42
43
44 /**
45 * @param OsBookingModel $booking
46 *
47 * @return mixed|void
48 *
49 * Returns deposit amount to charge in database format 1999.0000
50 *
51 */
52 public static function calculate_deposit_amount_to_charge( OsBookingModel $booking ) {
53 if ( ! $booking->service_id ) {
54 return 0;
55 }
56 $service = new OsServiceModel( $booking->service_id );
57 $amount = $service->get_deposit_amount_for_duration( $booking->duration );
58 $amount = apply_filters( 'latepoint_deposit_amount_for_service', $amount, $booking );
59 $amount = OsMoneyHelper::pad_to_db_format( $amount );
60
61 return $amount;
62 }
63
64
65 /**
66 * @param array $item_data
67 *
68 * @return OsBookingModel
69 */
70 public static function build_booking_model_from_item_data( array $item_data ): OsBookingModel {
71 $booking = new OsBookingModel();
72 if ( $item_data['id'] ) {
73 $booking = $booking->load_by_id( $item_data['id'] );
74 if ( ! $booking ) {
75 $booking = new OsBookingModel();
76 }
77 }
78 $booking->set_data( $item_data );
79 // get buffers from service and set to booking object
80 if ( ! isset( $item_data['buffer_before'] ) && ! isset( $item_data['buffer_after'] ) ) {
81 $booking->set_buffers();
82 }
83 if ( empty( $booking->end_time ) ) {
84 $booking->calculate_end_date_and_time();
85 }
86 if ( empty( $booking->end_date ) ) {
87 $booking->calculate_end_date();
88 }
89 $booking->set_utc_datetimes();
90
91 return $booking;
92 }
93
94 public static function get_booking_id_and_manage_ability_by_key( string $key ) {
95 $booking_id = OsMetaHelper::get_booking_id_by_meta_value( 'key_to_manage_for_agent', $key );
96 if ( $booking_id ) {
97 return [
98 'booking_id' => $booking_id,
99 'for' => 'agent',
100 ];
101 }
102
103 $booking_id = OsMetaHelper::get_booking_id_by_meta_value( 'key_to_manage_for_customer', $key );
104 if ( $booking_id ) {
105 return [
106 'booking_id' => $booking_id,
107 'for' => 'customer',
108 ];
109 }
110
111 return false;
112 }
113
114 public static function is_action_allowed( string $action, OsBookingModel $booking, string $key = '' ) {
115 $is_allowed = false;
116 if ( empty( $booking->id ) ) {
117 return false;
118 }
119 if ( ! in_array( $action, [ 'cancel', 'reschedule' ] ) ) {
120 return false;
121 }
122 $action_result = false;
123 switch ( $action ) {
124 case 'cancel':
125 $action_result = OsCustomerHelper::can_cancel_booking( $booking );
126 break;
127 case 'reschedule':
128 $action_result = OsCustomerHelper::can_reschedule_booking( $booking );
129 break;
130 }
131 if ( ! empty( $key ) ) {
132 // key is passed, check if allowed through key
133 $agent_key_meta = OsMetaHelper::get_booking_meta_by_key( 'key_to_manage_for_agent', $booking->id );
134 $customer_key_meta = OsMetaHelper::get_booking_meta_by_key( 'key_to_manage_for_customer', $booking->id );
135 if ( $key == $agent_key_meta ) {
136 // agent can do everything, no need to check for action
137 $is_allowed = true;
138 } elseif ( $key == $customer_key_meta ) {
139 // customer
140 $is_allowed = $action_result;
141 }
142 } elseif ( OsAuthHelper::get_logged_in_customer_id() == $booking->customer_id ) {
143 $is_allowed = $action_result;
144 }
145
146 return $is_allowed;
147 }
148
149 public static function generate_add_to_calendar_links( OsBookingModel $booking, $key = false ): string {
150 $html = '<div class="add-to-calendar-types">
151 <div class="atc-heading-wrapper">
152 <div class="atc-heading">' . esc_html__( 'Calendar Type', 'latepoint' ) . '</div>
153 <div class="close-calendar-types"></div>
154 </div>
155 <a href="' . esc_url( $booking->get_ical_download_link( $key ) ) . '" target="_blank" class="atc-type atc-type-apple">
156 <div class="atc-type-image"></div>
157 <div class="atc-type-name">' . esc_html__( 'Apple Calendar', 'latepoint' ) . '</div>
158 </a>
159 <a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'google' ) ) . '" target="_blank" class="atc-type atc-type-google">
160 <div class="atc-type-image"></div>
161 <div class="atc-type-name">' . esc_html__( 'Google Calendar', 'latepoint' ) . '</div>
162 </a>
163 <a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'outlook' ) ) . '" target="_blank" class="atc-type atc-type-outlook">
164 <div class="atc-type-image"></div>
165 <div class="atc-type-name">' . esc_html__( 'Outlook.com', 'latepoint' ) . '</div>
166 </a>
167 <a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'outlook' ) ) . '" target="_blank" class="atc-type atc-type-office-365">
168 <div class="atc-type-image"></div>
169 <div class="atc-type-name">' . esc_html__( 'Microsoft 365', 'latepoint' ) . '</div>
170 </a>
171 </div>';
172
173 return $html;
174 }
175
176
177 public static function get_bookings_for_select( $should_be_in_future = false ) {
178 $bookings = new OsBookingModel();
179 if ( $should_be_in_future ) {
180 $bookings = $bookings->should_be_in_future();
181 }
182 $bookings = $bookings->order_by( 'id desc' )->set_limit( 100 )->get_results_as_models();
183 $bookings_options = [];
184 foreach ( $bookings as $booking ) {
185 $name = $booking->service->name . ', ' . $booking->agent->full_name . ', ' . $booking->customer->full_name . ' [' . $booking->booking_code . ' : ' . $booking->id . ']';
186 $bookings_options[] = [
187 'value' => $booking->id,
188 'label' => esc_html( $name ),
189 ];
190 }
191
192 return $bookings_options;
193 }
194
195 /**
196 *
197 * Determine whether to show
198 *
199 * @param $rows
200 *
201 * @return bool
202 */
203 public static function is_breakdown_free( $rows ) {
204 return ( ( empty( $rows['subtotal']['raw_value'] ) || ( (float) $rows['subtotal']['raw_value'] <= 0 ) ) && ( empty( $rows['total']['raw_value'] ) || ( (float) $rows['total']['raw_value'] <= 0 ) ) );
205 }
206
207 public static function output_price_breakdown( $rows ) {
208 foreach ( $rows['before_subtotal'] as $row ) {
209 self::output_price_breakdown_row( $row );
210 }
211 // if there is nothing between subtotal and total - don't show subtotal as it will be identical to total
212 if ( ! empty( $rows['after_subtotal'] ) ) {
213 if ( ! empty( $rows['subtotal'] ) ) {
214 echo '<div class="subtotal-separator"></div>';
215 self::output_price_breakdown_row( $rows['subtotal'] );
216 }
217 foreach ( $rows['after_subtotal'] as $row ) {
218 self::output_price_breakdown_row( $row );
219 }
220 }
221 if ( ! empty( $rows['total'] ) ) {
222 self::output_price_breakdown_row( $rows['total'] );
223 }
224 if ( ! empty( $rows['payments'] ) ) {
225 foreach ( $rows['payments'] as $row ) {
226 self::output_price_breakdown_row( $row );
227 }
228 }
229 if ( ! empty( $rows['balance'] ) ) {
230 self::output_price_breakdown_row( $rows['balance'] );
231 }
232 }
233
234 public static function output_price_breakdown_row( $row ) {
235 if ( ! empty( $row['items'] ) ) {
236 if ( ! empty( $row['heading'] ) ) {
237 echo '<div class="summary-box-heading"><div class="sbh-item">' . esc_html( $row['heading'] ) . '</div><div class="sbh-line"></div></div>';
238 }
239 foreach ( $row['items'] as $row_item ) {
240 self::output_price_breakdown_row( $row_item );
241 }
242 } else {
243 $extra_class = '';
244 if ( isset( $row['style'] ) && $row['style'] == 'strong' ) {
245 $extra_class .= ' spi-strong';
246 }
247 if ( isset( $row['style'] ) && $row['style'] == 'total' ) {
248 $extra_class .= ' spi-total';
249 }
250 if ( isset( $row['type'] ) && $row['type'] == 'credit' ) {
251 $extra_class .= ' spi-positive';
252 }
253 ?>
254 <div class="summary-price-item-w <?php echo esc_attr( $extra_class ); ?>">
255 <div class="spi-name">
256 <?php echo $row['label']; ?>
257 <?php if ( ! empty( $row['note'] ) ) {
258 echo '<span class="pi-note">' . esc_html( $row['note'] ) . '</span>';
259 } ?>
260 <?php if ( ! empty( $row['badge'] ) ) {
261 echo '<span class="pi-badge">' . esc_html( $row['badge'] ) . '</span>';
262 } ?>
263 </div>
264 <div class="spi-price"><?php echo esc_html( $row['value'] ); ?></div>
265 </div>
266 <?php
267 }
268 }
269
270 public static function output_price_breakdown_row_as_input_field( $row, $base_name ) {
271 $field_name = $base_name . '[' . OsUtilHelper::random_text( 'alnum', 8 ) . ']';
272 if ( ! empty( $row['items'] ) ) {
273 echo OsFormHelper::hidden_field( $field_name . '[heading]', $row['heading'] ?? '' );
274 foreach ( $row['items'] as $row_item ) {
275 self::output_price_breakdown_row_as_input_field( $row_item, $field_name . '[items]' );
276 }
277 } else {
278 $is_negative = ( $row['raw_value'] < 0 );
279 $wrapper_class = $is_negative ? [ 'class' => 'green-value-input' ] : [];
280 $label = $row['label'] ?? '';
281 if ( ! empty( $row['note'] ) ) {
282 $label .= ' ' . $row['note'];
283 }
284 // Use abs() for money_field because inputmask cannot parse negative initial values; the sign is preserved via the 'type' hidden field
285 $field_value = $is_negative ? abs( $row['raw_value'] ) : $row['raw_value'];
286 echo OsFormHelper::money_field( $field_name . '[value]', $label, $field_value, [ 'theme' => 'right-aligned' ], [], $wrapper_class );
287 echo OsFormHelper::hidden_field( $field_name . '[label]', $row['label'] ?? '' );
288 echo OsFormHelper::hidden_field( $field_name . '[style]', $row['style'] ?? '' );
289 echo OsFormHelper::hidden_field( $field_name . '[type]', $row['type'] ?? '' );
290 echo OsFormHelper::hidden_field( $field_name . '[note]', $row['note'] ?? '' );
291 echo OsFormHelper::hidden_field( $field_name . '[badge]', $row['badge'] ?? '' );
292 }
293 if ( ! empty( $row['sub_items'] ) ) {
294 foreach ( $row['sub_items'] as $row_item ) {
295 self::output_price_breakdown_row_as_input_field( $row_item, $field_name . '[sub_items]' );
296 }
297 }
298 }
299
300 /**
301 * @param \LatePoint\Misc\Filter $filter
302 * @param bool $accessed_from_backend
303 *
304 * @return array
305 */
306 public static function get_blocked_periods_grouped_by_day( \LatePoint\Misc\Filter $filter, bool $accessed_from_backend = false ): array {
307 $grouped_blocked_periods = [];
308
309 if ( $filter->date_from ) {
310 $date_from = OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
311 $date_to = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
312
313 # Loop through days to fill in days that might have no bookings
314 for ( $day = clone $date_from; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
315 $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] = [];
316 }
317 }
318 if ( ! $accessed_from_backend ) {
319 $today = new OsWpDateTime( 'today' );
320 $earliest_possible_booking = OsSettingsHelper::get_earliest_possible_booking_restriction( $filter->service_id );
321
322 $block_end_datetime = OsTimeHelper::now_datetime_object();
323 if ( $earliest_possible_booking ) {
324 try {
325 $block_end_datetime->modify( $earliest_possible_booking );
326 } catch ( Exception $e ) {
327 $block_end_datetime = OsTimeHelper::now_datetime_object();
328 }
329 }
330 for ( $day = clone $today; $day->format( 'Y-m-d' ) <= $block_end_datetime->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
331 // loop days from now to the earliest possible booking and block timeslots if these days were actually requested
332 if ( isset( $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] ) ) {
333 $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ][] = new \LatePoint\Misc\BlockedPeriod(
334 [
335 'start_time' => 0,
336 'end_time' => ( $day->format( 'Y-m-d' ) < $block_end_datetime->format( 'Y-m-d' ) ) ? 24 * 60 : OsTimeHelper::convert_datetime_to_minutes( $block_end_datetime ),
337 'start_date' => $day->format( 'Y-m-d' ),
338 'end_date' => $day->format( 'Y-m-d' ),
339 ]
340 );
341 }
342 }
343 $latest_possible_booking = OsSettingsHelper::get_latest_possible_booking_restriction( $filter->service_id );
344 if ( $latest_possible_booking ) {
345 try {
346 $latest_booking_datetime = OsTimeHelper::now_datetime_object();
347 $latest_booking_datetime->modify( $latest_possible_booking );
348 } catch ( Exception $e ) {
349 $latest_booking_datetime = null;
350 }
351 if ( $latest_booking_datetime && $filter->date_from ) {
352 $date_to = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
353 // Start from the latest_booking_datetime day
354 for ( $day = clone $latest_booking_datetime; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
355 if ( isset( $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] ) ) {
356 $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ][] = new \LatePoint\Misc\BlockedPeriod(
357 [
358 'start_time' => ( $day->format( 'Y-m-d' ) == $latest_booking_datetime->format( 'Y-m-d' ) ) ? OsTimeHelper::convert_datetime_to_minutes( $latest_booking_datetime ) : 0,
359 'end_time' => 24 * 60,
360 'start_date' => $day->format( 'Y-m-d' ),
361 'end_date' => $day->format( 'Y-m-d' ),
362 ]
363 );
364 }
365 }
366 }
367 }
368 }
369
370 $grouped_blocked_periods = apply_filters( 'latepoint_blocked_periods_for_range', $grouped_blocked_periods, $filter );
371
372 return $grouped_blocked_periods;
373 }
374
375 /**
376 * @param \LatePoint\Misc\Filter $filter
377 *
378 * @return array
379 */
380 public static function get_booked_periods_grouped_by_day( \LatePoint\Misc\Filter $filter ): array {
381 $booked_periods = self::get_booked_periods( $filter );
382
383 $grouped_booked_periods = [];
384 if ( $filter->date_from ) {
385 $date_from = OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
386 $date_to = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
387
388 # Loop through days to fill in days that might have no bookings
389 for ( $day = clone $date_from; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
390 $grouped_booked_periods[ $day->format( 'Y-m-d' ) ] = [];
391 }
392 foreach ( $booked_periods as $booked_period ) {
393 $grouped_booked_periods[ $booked_period->start_date ][] = $booked_period;
394 // if event spans multiple days - add to other days as well
395 if ( $booked_period->end_date && ( $booked_period->start_date != $booked_period->end_date ) ) {
396 $grouped_booked_periods[ $booked_period->end_date ][] = $booked_period;
397 }
398 }
399 }
400
401 return $grouped_booked_periods;
402 }
403
404 /**
405 * @param \LatePoint\Misc\Filter $filter
406 *
407 * @return \LatePoint\Misc\BookedPeriod[]
408 */
409 public static function get_booked_periods( \LatePoint\Misc\Filter $filter ): array {
410
411
412 $bookings = self::get_bookings( $filter, true );
413 $booked_periods = [];
414
415 foreach ( $bookings as $booking ) {
416 $booked_periods[] = \LatePoint\Misc\BookedPeriod::create_from_booking_model( $booking );
417 }
418
419
420 if ( $filter->consider_cart_items ) {
421 $cart = OsCartsHelper::get_or_create_cart();
422 $bookings_in_cart = $cart->get_bookings_from_cart_items();
423
424 foreach ( $bookings_in_cart as $cart_booking ) {
425 $booked_periods[] = \LatePoint\Misc\BookedPeriod::create_from_booking_model( $cart_booking );
426 }
427 }
428
429 // TODO Update all filters to accept new "filter" variable (In Google Calendar addon)
430 $booked_periods = apply_filters( 'latepoint_get_booked_periods', $booked_periods, $filter );
431
432 return $booked_periods;
433 }
434
435
436 /**
437 * @param \LatePoint\Misc\Filter $filter
438 * @param bool $as_models
439 *
440 * @return array
441 */
442 public static function get_bookings( \LatePoint\Misc\Filter $filter, bool $as_models = false ): array {
443 $bookings = new OsBookingModel();
444 if ( $filter->date_from ) {
445 if ( $filter->date_from && $filter->date_to ) {
446 # both start and end date provided - means it's a range
447 $bookings->where(
448 [
449 'start_date >=' => $filter->date_from,
450 'start_date <=' => $filter->date_to,
451 ]
452 );
453 } else {
454 # only start_date provided - means it's a specific date requested
455 $bookings->where( [ 'start_date' => $filter->date_from ] );
456 }
457 }
458
459
460 if ( $filter->connections ) {
461 $connection_conditions = [];
462 foreach ( $filter->connections as $connection ) {
463 $connection_conditions[] = [
464 'AND' =>
465 [
466 'agent_id' => $connection->agent_id,
467 'service_id' => $connection->service_id,
468 'location_id' => $connection->location_id,
469 ],
470 ];
471 }
472 $bookings->where( [ 'OR' => $connection_conditions ] );
473 } else {
474 if ( $filter->agent_id ) {
475 $bookings->where( [ 'agent_id' => $filter->agent_id ] );
476 }
477 if ( $filter->location_id ) {
478 $bookings->where( [ 'location_id' => $filter->location_id ] );
479 }
480 if ( $filter->service_id ) {
481 $bookings->where( [ 'service_id' => $filter->service_id ] );
482 }
483 }
484 if ( $filter->statuses ) {
485 $bookings->where( [ 'status' => $filter->statuses ] );
486 }
487 if ( $filter->exclude_booking_ids ) {
488 $bookings->where( [ 'id NOT IN' => $filter->exclude_booking_ids ] );
489 }
490 $bookings->order_by( 'start_time asc, end_time asc, service_id asc' );
491 $bookings = ( $as_models ) ? $bookings->get_results_as_models() : $bookings->get_results();
492
493 // make sure to return empty array if nothing is found
494 if ( empty( $bookings ) ) {
495 $bookings = [];
496 }
497
498 return $bookings;
499 }
500
501 public static function generate_ical_event_string( $booking ) {
502 // translators: %1$s is agent name, %2$s is service name
503 $booking_description = sprintf( __( 'Appointment with %1$s for %2$s', 'latepoint' ), $booking->agent->full_name, $booking->service->name );
504
505 $ics = new ICS(
506 array(
507 'location' => $booking->location->full_address,
508 'description' => '',
509 'dtstart' => $booking->format_start_date_and_time_for_google(),
510 'dtend' => $booking->format_end_date_and_time_for_google(),
511 'summary' => $booking_description,
512 'url' => get_site_url(),
513 )
514 );
515
516 return $ics->to_string();
517 }
518
519 /**
520 * @param \LatePoint\Misc\BookingRequest $booking_request
521 *
522 * @return bool
523 *
524 * Checks if requested booking slot is available, loads work periods and booked periods from database and checks availability against them
525 */
526 public static function is_booking_request_available( \LatePoint\Misc\BookingRequest $booking_request, $settings = [] ): bool {
527 try {
528 $requested_date = new OsWpDateTime( $booking_request->start_date );
529 } catch ( Exception $e ) {
530 return false;
531 }
532 $resources = OsResourceHelper::get_resources_grouped_by_day( $booking_request, $requested_date, $requested_date, $settings );
533 if ( empty( $resources[ $requested_date->format( 'Y-m-d' ) ] ) ) {
534 return false;
535 }
536 $is_available = false;
537
538
539 // check if satisfies earliest and latest bookings - check per-service settings first, then global
540 $earliest_possible_booking = OsSettingsHelper::get_earliest_possible_booking_restriction( $booking_request->service_id );
541 $latest_possible_booking = OsSettingsHelper::get_latest_possible_booking_restriction( $booking_request->service_id );
542
543
544 if ( $earliest_possible_booking || $latest_possible_booking ) {
545 // check earliest
546 if ( ! empty( $earliest_possible_booking ) ) {
547 try {
548 $earliest_possible_booking_date = new OsWpDateTime( $earliest_possible_booking );
549 if ( $earliest_possible_booking_date > $booking_request->get_start_datetime() ) {
550 return false;
551 }
552 } catch ( Exception $e ) {
553
554 }
555 }
556 if ( ! empty( $latest_possible_booking ) ) {
557 // check latest
558 try {
559 $latest_possible_booking_date = new OsWpDateTime( $latest_possible_booking );
560 if ( $latest_possible_booking_date < $booking_request->get_start_datetime() ) {
561 return false;
562 }
563 } catch ( Exception $e ) {
564
565 }
566 }
567 }
568
569 foreach ( $resources[ $requested_date->format( 'Y-m-d' ) ] as $resource ) {
570 foreach ( $resource->slots as $slot ) {
571 if ( $slot->start_time == $booking_request->start_time && $slot->can_accomodate( $booking_request->total_attendees ) ) {
572 $is_available = true;
573 }
574 if ( $is_available ) {
575 break;
576 }
577 }
578 if ( $is_available ) {
579 break;
580 }
581 }
582
583 return $is_available;
584 }
585
586 /**
587 *
588 * Checks if two bookings are part of the same group appointment
589 *
590 * @param bool|OsBookingModel $booking
591 * @param bool|OsBookingModel $compare_booking
592 *
593 * @return bool
594 */
595 public static function check_if_group_bookings( $booking, $compare_booking ): bool {
596 if ( $booking && $compare_booking && ( $compare_booking->start_time == $booking->start_time ) && ( $compare_booking->end_time == $booking->end_time ) && ( $compare_booking->service_id == $booking->service_id ) && ( $compare_booking->location_id == $booking->location_id ) ) {
597 return true;
598 } else {
599 return false;
600 }
601 }
602
603
604 public static function process_actions_after_save( $booking_id ) {
605 }
606
607 /**
608 * @param DateTime $start_date
609 * @param DateTime $end_date
610 * @param \LatePoint\Misc\BookingRequest $booking_request
611 * @param \LatePoint\Misc\BookingResource[] $resources
612 * @param array $settings
613 *
614 * @return string
615 * @throws Exception
616 */
617 public static function get_quick_availability_days( DateTime $start_date, DateTime $end_date, \LatePoint\Misc\BookingRequest $booking_request, array $resources = [], array $settings = [] ) {
618 $default_settings = [
619 'work_boundaries' => false,
620 'exclude_booking_ids' => [],
621 ];
622 $settings = array_merge( $default_settings, $settings );
623
624 $html = '';
625
626 if ( ! $resources ) {
627 $resources = OsResourceHelper::get_resources_grouped_by_day( $booking_request, $start_date, $end_date, $settings );
628 }
629 if ( ! $settings['work_boundaries'] ) {
630 $settings['work_boundaries'] = OsResourceHelper::get_work_boundaries_for_groups_of_resources( $resources );
631 }
632
633 if ( $start_date->format( 'j' ) != '1' ) {
634 $html .= '<div class="ma-month-label">' . OsUtilHelper::get_month_name_by_number( $start_date->format( 'n' ) ) . '</div>';
635 }
636
637 for ( $day_date = clone $start_date; $day_date <= $end_date; $day_date->modify( '+1 day' ) ) {
638 // first day of month, output month name
639 if ( $day_date->format( 'j' ) == '1' ) {
640 $html .= '<div class="ma-month-label">' . OsUtilHelper::get_month_name_by_number( $day_date->format( 'n' ) ) . '</div>';
641 }
642 $html .= '<div class="ma-day ma-day-number-' . $day_date->format( 'N' ) . '">';
643 $html .= '<div class="ma-day-info">';
644 $html .= '<span class="ma-day-number">' . $day_date->format( 'j' ) . '</span>';
645 $html .= '<span class="ma-day-weekday">' . OsUtilHelper::get_weekday_name_by_number( $day_date->format( 'N' ), true ) . '</span>';
646 $html .= '</div>';
647 $html .= OsTimelineHelper::availability_timeline( $booking_request, $settings['work_boundaries'], $resources[ $day_date->format( 'Y-m-d' ) ], [ 'book_on_click' => false ] );
648 $html .= '</div>';
649 }
650
651 return $html;
652 }
653
654 public static function count_pending_bookings() {
655 $bookings = new OsBookingModel();
656
657 return $bookings->filter_allowed_records()->where( [ 'status IN' => OsBookingHelper::get_booking_statuses_for_pending_page() ] )->count();
658 }
659
660
661 public static function generate_bundles_folder( ?array $bundles = null, array $restrictions = [] ): void {
662 if ( null === $bundles ) {
663 $bundles_model = new OsBundleModel();
664 $bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
665 }
666
667 /**
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 . '.subtotal)';
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 public static function generate_summary_actions_for_booking( OsBookingModel $booking, ?string $key = null ) {
1751 ?>
1752 <div class="booking-full-summary-actions">
1753 <div class="add-to-calendar-wrapper">
1754 <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>
1755 <?php echo OsBookingHelper::generate_add_to_calendar_links( $booking, $key ?? $booking->get_key_to_manage_for( 'customer' ) ); ?>
1756 </div>
1757 <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>
1758 <?php
1759 if ( $booking->is_upcoming() ) {
1760 if ( OsCustomerHelper::can_reschedule_booking( $booking ) ) { ?>
1761 <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">
1762 <i class="latepoint-icon latepoint-icon-calendar"></i>
1763 <span><?php esc_html_e( 'Reschedule', 'latepoint' ); ?></span>
1764 </a>
1765 <?php
1766 }
1767 if ( OsCustomerHelper::can_cancel_booking( $booking ) ) { ?>
1768 <a href="#" class="booking-summary-action-btn cancel-appointment-btn"
1769 data-os-prompt="<?php esc_attr_e( 'Are you sure you want to cancel this appointment?', 'latepoint' ); ?>"
1770 data-os-success-action="reload"
1771 data-os-action="<?php echo esc_attr( OsRouterHelper::build_route_name( 'manage_booking_by_key', 'request_cancellation' ) ); ?>"
1772 data-os-params="<?php echo esc_attr( OsUtilHelper::build_os_params( [ 'key' => $key ?? $booking->get_key_to_manage_for( 'customer' ) ], 'cancel_booking_' . $booking->id ) ); ?>">
1773 <i class="latepoint-icon latepoint-icon-ui-24"></i>
1774 <span><?php esc_html_e( 'Cancel', 'latepoint' ); ?></span>
1775 </a>
1776 <?php
1777 }
1778 }
1779 do_action( 'latepoint_booking_summary_after_booking_actions', $booking );
1780 ?>
1781 </div>
1782 <?php
1783 }
1784
1785 public static function generate_summary_for_booking( OsBookingModel $booking, $cart_item_id = false, ?string $viewer = 'customer' ): string {
1786 $summary_html = '';
1787 $summary_html .= apply_filters( 'latepoint_booking_summary_before_summary_box', '', $booking );
1788 $summary_html .= '<div class="summary-box main-box" ' . ( ( $cart_item_id ) ? 'data-cart-item-id="' . $cart_item_id . '"' : '' ) . '>';
1789 $output_timezone_name = $viewer == 'customer' ? $booking->get_customer_timezone_name() : OsTimeHelper::get_wp_timezone_name();
1790 if ( ! empty( $booking->start_datetime_utc ) ) {
1791 $summary_html .= '<div class="summary-box-booking-date-box">';
1792 $summary_html .= '<div class="summary-box-booking-date-day">' . $booking->start_datetime_in_format( 'j', $output_timezone_name ) . '</div>';
1793 $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>';
1794 $summary_html .= '</div>';
1795 }
1796 $summary_html .= '<div class="summary-box-inner">';
1797 $service_headings = [];
1798 $service_headings = apply_filters( 'latepoint_booking_summary_service_headings', $service_headings, $booking );
1799 if ( $service_headings ) {
1800 $summary_html .= '<div class="summary-box-heading">';
1801 foreach ( $service_headings as $heading ) {
1802 $summary_html .= '<div class="sbh-item">' . $heading . '</div>';
1803 }
1804 $summary_html .= '<div class="sbh-line"></div>';
1805 $summary_html .= '</div>';
1806 }
1807 $summary_html .= '<div class="summary-box-content os-cart-item">';
1808 if ( $cart_item_id && OsCartsHelper::can_checkout_multiple_items() ) {
1809 $summary_html .= '<div class="os-remove-item-from-cart" role="button" tabindex="0" data-confirm-text="' . __( 'Are you sure you want to remove this item from your cart?', 'latepoint' ) . '" data-cart-item-id="' . $cart_item_id . '" data-route="' . OsRouterHelper::build_route_name( 'carts', 'remove_item_from_cart' ) . '">
1810 <div class="os-remove-from-cart-icon"></div>
1811 </div>';
1812 }
1813 $summary_html .= '<div class="sbc-big-item">' . $booking->get_service_name_for_summary() . '</div>';
1814 if ( $booking->start_date ) {
1815 $summary_html .= '<div class="sbc-highlighted-item">' . $booking->get_nice_datetime_for_summary( $viewer ) . '</div>';
1816 }
1817 /**
1818 * Output summary of the booking data after a start date and time
1819 *
1820 * @since 5.2.0
1821 * @hook latepoint_summary_booking_info_after_start_date
1822 *
1823 * @param {string} $summary_html HTML of the summary
1824 * @param {OsBookingModel} $booking Booking object that is being outputted
1825 * @param {string} $cart_item_id ID of a cart item this booking belongs to
1826 * @param {string} $viewer determines who is viewing this summary, can be customer or agent
1827 *
1828 * @returns {string} Filtered HTML
1829 */
1830 $summary_html = apply_filters( 'latepoint_summary_booking_info_after_start_date', $summary_html, $booking, $cart_item_id, $viewer );
1831 $summary_html .= '</div>';
1832
1833 $service_attributes = [];
1834 $service_attributes = apply_filters( 'latepoint_booking_summary_service_attributes', $service_attributes, $booking );
1835 if ( $service_attributes ) {
1836 $summary_html .= '<div class="summary-attributes sa-clean">';
1837 foreach ( $service_attributes as $attribute ) {
1838 $summary_html .= '<span>' . $attribute['label'] . ': <strong>' . $attribute['value'] . '</strong></span>';
1839 }
1840 $summary_html .= '</div>';
1841 }
1842 $summary_html .= '</div>';
1843 $summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box_inner', '', $booking );
1844 $summary_html .= '</div>';
1845 $summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box', '', $booking );
1846
1847 return $summary_html;
1848 }
1849
1850
1851 /**
1852 * @param OsBookingModel[] $bookings
1853 *
1854 * @return bool
1855 */
1856 public static function bookings_have_same_agent( array $bookings ): bool {
1857 return ( count( array_unique( array_column( $bookings, 'agent_id' ) ) ) == 1 );
1858 }
1859
1860 /**
1861 * @param OsBookingModel[] $bookings
1862 *
1863 * @return bool
1864 */
1865 public static function bookings_have_same_location( array $bookings ): bool {
1866 return ( count( array_unique( array_column( $bookings, 'location_id' ) ) ) == 1 );
1867 }
1868
1869 /**
1870 * @param OsBookingModel[] $bookings
1871 *
1872 * @return bool
1873 */
1874 public static function bookings_have_same_service( array $bookings ): bool {
1875 return ( count( array_unique( array_column( $bookings, 'service_id' ) ) ) == 1 );
1876 }
1877
1878 public static function prepare_new_from_params( array $params ): OsBookingModel {
1879 $booking = new OsBookingModel();
1880
1881 $services = OsServiceHelper::get_allowed_active_services();
1882 $agents = OsAgentHelper::get_allowed_active_agents();
1883
1884 // LOAD FROM PASSED PARAMS
1885 $booking->order_item_id = $params['order_item_id'] ?? '';
1886 $booking->service_id = ! empty( $params['service_id'] ) ? OsUtilHelper::first_value_if_array( $params['service_id'] ) : '';
1887 if ( empty( $booking->service_id ) && ! empty( $services ) ) {
1888 $booking->service_id = $services[0]->id;
1889 }
1890
1891 $booking->agent_id = ! empty( $params['agent_id'] ) ? OsUtilHelper::first_value_if_array( $params['agent_id'] ) : '';
1892 if ( empty( $booking->agent_id ) && ! empty( $agents ) ) {
1893 $booking->agent_id = $agents[0]->id;
1894 }
1895
1896 if ( ! empty( $params['order_id'] ) ) {
1897 $order = new OsOrderModel( $params['order_id'] );
1898 $booking->customer_id = $order->customer_id;
1899 } else {
1900 $booking->customer_id = ! empty( $params['customer_id'] ) ? OsUtilHelper::first_value_if_array( $params['customer_id'] ) : '';
1901 }
1902
1903 $booking->location_id = ! empty( $params['location_id'] ) ? OsUtilHelper::first_value_if_array( $params['location_id'] ) : OsLocationHelper::get_default_location_id( true );
1904 $booking->start_date = $params['start_date'] ?? OsTimeHelper::today_date( 'Y-m-d' );
1905 $booking->start_time = $params['start_time'] ?? 600;
1906
1907 $booking->end_time = ( $booking->service_id ) ? $booking->calculate_end_time() : $booking->start_time + 60;
1908 $booking->end_date = $booking->calculate_end_date();
1909 $booking->buffer_before = ( $booking->service_id ) ? $booking->service->buffer_before : 0;
1910 $booking->buffer_after = ( $booking->service_id ) ? $booking->service->buffer_after : 0;
1911 $booking->status = LATEPOINT_BOOKING_STATUS_APPROVED;
1912
1913 return $booking;
1914 }
1915
1916 /**
1917 * Returns the <th> HTML for a single column header cell in the bookings table.
1918 *
1919 * @param string $col_key Unified column key (e.g. 'id', 'datetime', 'customer.email').
1920 * @param array $col_def Column definition from get_all_bookings_table_columns().
1921 * @param string $order_key Currently active sort key.
1922 * @param string $order_dir Currently active sort direction ('asc'|'desc').
1923 * @return string
1924 */
1925 public static function render_table_header_cell( string $col_key, array $col_def, string $order_key, string $order_dir ): string {
1926 switch ( $col_key ) {
1927 case 'id':
1928 $active_class = ( $order_key === 'booking_id' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1929 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_id">' . esc_html__( 'ID', 'latepoint' ) . '</th>';
1930
1931 case 'service':
1932 return '<th>' . esc_html__( 'Service', 'latepoint' ) . '</th>';
1933
1934 case 'datetime':
1935 $active_class = ( $order_key === 'booking_start_datetime' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1936 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_start_datetime">' . esc_html__( 'Date/Time', 'latepoint' ) . '</th>';
1937
1938 case 'time_left':
1939 $active_class = ( $order_key === 'booking_time_left' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1940 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_time_left">' . esc_html__( 'Time Left', 'latepoint' ) . '</th>';
1941
1942 case 'agent':
1943 return '<th>' . esc_html__( 'Agent', 'latepoint' ) . '</th>';
1944
1945 case 'location':
1946 return '<th>' . esc_html__( 'Location', 'latepoint' ) . '</th>';
1947
1948 case 'customer':
1949 return '<th>' . esc_html__( 'Customer', 'latepoint' ) . '</th>';
1950
1951 case 'status':
1952 return '<th>' . esc_html__( 'Status', 'latepoint' ) . '</th>';
1953
1954 case 'payment_status':
1955 return '<th>' . esc_html__( 'Payment Status', 'latepoint' ) . '</th>';
1956
1957 case 'created_on':
1958 $active_class = ( $order_key === 'booking_created_on' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
1959 return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_created_on">' . esc_html__( 'Created On', 'latepoint' ) . '</th>';
1960
1961 default:
1962 // Extra columns.
1963 return '<th>' . esc_html( $col_def['label'] ) . '</th>';
1964 }
1965 }
1966
1967 /**
1968 * Returns the <th> HTML for a single column filter cell in the bookings table header.
1969 *
1970 * @param string $col_key Unified column key.
1971 * @param array $col_def Column definition.
1972 * @param array $services_list Keyed list of services for the service filter select.
1973 * @param array $agents_list Keyed list of agents for the agent filter select.
1974 * @param array $locations_list Keyed list of locations for the location filter select.
1975 * @return string
1976 */
1977 public static function render_table_filter_cell( string $col_key, array $col_def, array $services_list, array $agents_list, array $locations_list ): string {
1978 switch ( $col_key ) {
1979 case 'id':
1980 return '<th>' . OsFormHelper::text_field(
1981 'filter[id]',
1982 false,
1983 '',
1984 [
1985 'placeholder' => __( 'ID', 'latepoint' ),
1986 'theme' => 'bordered',
1987 'style' => 'width: 60px;',
1988 'class' => 'os-table-filter',
1989 ]
1990 ) . '</th>';
1991
1992 case 'service':
1993 return '<th>' . OsFormHelper::select_field(
1994 'filter[service_id]',
1995 false,
1996 $services_list,
1997 '',
1998 [
1999 'placeholder' => __( 'All Services', 'latepoint' ),
2000 'class' => 'os-table-filter',
2001 ]
2002 ) . '</th>';
2003
2004 case 'datetime':
2005 ob_start();
2006 ?>
2007 <th>
2008 <div class="os-form-group">
2009 <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' ); ?>">
2010 <span class="range-picker-value"><?php esc_html_e( 'Filter Date', 'latepoint' ); ?></span>
2011 <i class="latepoint-icon latepoint-icon-chevron-down"></i>
2012 <input type="hidden" class="os-table-filter os-datepicker-date-from" name="filter[booking_date_from]" value=""/>
2013 <input type="hidden" class="os-table-filter os-datepicker-date-to" name="filter[booking_date_to]" value=""/>
2014 </div>
2015 </div>
2016 </th>
2017 <?php
2018 return ob_get_clean();
2019
2020 case 'time_left':
2021 return '<th>' . OsFormHelper::select_field(
2022 'filter[time_status]',
2023 false,
2024 [
2025 'upcoming' => __( 'Upcoming', 'latepoint' ),
2026 'past' => __( 'Past', 'latepoint' ),
2027 'now' => __( 'Happening Now', 'latepoint' ),
2028 ],
2029 '',
2030 [
2031 'placeholder' => __( 'Show All', 'latepoint' ),
2032 'class' => 'os-table-filter',
2033 ]
2034 ) . '</th>';
2035
2036 case 'agent':
2037 return '<th>' . OsFormHelper::select_field(
2038 'filter[agent_id]',
2039 false,
2040 $agents_list,
2041 '',
2042 [
2043 'placeholder' => __( 'All Agents', 'latepoint' ),
2044 'class' => 'os-table-filter',
2045 ]
2046 ) . '</th>';
2047
2048 case 'location':
2049 return '<th>' . OsFormHelper::select_field(
2050 'filter[location_id]',
2051 false,
2052 $locations_list,
2053 '',
2054 [
2055 'placeholder' => __( 'All Locations', 'latepoint' ),
2056 'class' => 'os-table-filter',
2057 ]
2058 ) . '</th>';
2059
2060 case 'customer':
2061 return '<th>' . OsFormHelper::text_field(
2062 'filter[customer][full_name]',
2063 false,
2064 '',
2065 [
2066 'class' => 'os-table-filter',
2067 'theme' => 'bordered',
2068 'placeholder' => __( 'Search by Customer', 'latepoint' ),
2069 ]
2070 ) . '</th>';
2071
2072 case 'status':
2073 return '<th>' . OsFormHelper::select_field(
2074 'filter[status]',
2075 false,
2076 OsBookingHelper::get_statuses_list(),
2077 '',
2078 [
2079 'placeholder' => __( 'Show All', 'latepoint' ),
2080 'class' => 'os-table-filter',
2081 ]
2082 ) . '</th>';
2083
2084 case 'payment_status':
2085 return '<th>' . OsFormHelper::select_field(
2086 'filter[order][payment_status]',
2087 false,
2088 OsOrdersHelper::get_order_payment_statuses_list(),
2089 '',
2090 [
2091 'placeholder' => __( 'Show All', 'latepoint' ),
2092 'class' => 'os-table-filter',
2093 ]
2094 ) . '</th>';
2095
2096 case 'created_on':
2097 ob_start();
2098 ?>
2099 <th>
2100 <div class="os-form-group">
2101 <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' ); ?>">
2102 <span class="range-picker-value"><?php esc_html_e( 'Filter Date', 'latepoint' ); ?></span>
2103 <i class="latepoint-icon latepoint-icon-chevron-down"></i>
2104 <input type="hidden" class="os-table-filter os-datepicker-date-from" name="filter[created_date_from]" value=""/>
2105 <input type="hidden" class="os-table-filter os-datepicker-date-to" name="filter[created_date_to]" value=""/>
2106 </div>
2107 </div>
2108 </th>
2109 <?php
2110 return ob_get_clean();
2111
2112 default:
2113 // Extra columns.
2114 if ( 'extra' !== $col_def['type'] ) {
2115 return '<th></th>';
2116 }
2117 $extra_type = $col_def['extra_type'];
2118 $extra_key = $col_def['extra_key'];
2119 $extra_label = $col_def['label'];
2120 $field_name = ( 'booking' !== $extra_type ) ? 'filter[' . $extra_type . '][' . $extra_key . ']' : 'filter[' . $extra_key . ']';
2121 // Skip the filter input for magic properties that can't be queried.
2122 if ( 'booking' === $extra_type && ! property_exists( 'OsBookingModel', $extra_key ) ) {
2123 return '<th></th>';
2124 }
2125 return '<th>' . OsFormHelper::text_field(
2126 $field_name,
2127 false,
2128 '',
2129 [
2130 'class' => 'os-table-filter',
2131 'theme' => 'bordered',
2132 'placeholder' => $extra_label,
2133 ]
2134 ) . '</th>';
2135 }
2136 }
2137
2138 /**
2139 * Returns the <td> HTML for a single data cell in the bookings table body.
2140 *
2141 * @param string $col_key Unified column key.
2142 * @param array $col_def Column definition.
2143 * @param OsBookingModel $booking Current booking row.
2144 * @return string
2145 */
2146 public static function render_table_body_cell( string $col_key, array $col_def, OsBookingModel $booking ): string {
2147 switch ( $col_key ) {
2148 case 'id':
2149 return '<td class="os-column-faded text-right has-floating-button">' .
2150 esc_html( $booking->id ) .
2151 '<div class="os-floating-button"><i class="latepoint-icon latepoint-icon-edit-3"></i></div>' .
2152 '</td>';
2153
2154 case 'service':
2155 return '<td><div class="os-with-service-color"><span class="cell-link-content">' .
2156 '<span class="os-column-service-color" style="background-color: ' . esc_attr( $booking->service->bg_color ) . '"></span>' .
2157 '<span>' . esc_html( $booking->service->name ) . '</span>' .
2158 '</span></div></td>';
2159
2160 case 'datetime':
2161 return '<td><strong>' . esc_html( $booking->nice_start_date ) . '</strong> <span class="os-dot"></span> <span>' . esc_html( $booking->nice_start_time ) . '</span></td>';
2162
2163 case 'time_left':
2164 $time_html = '';
2165 switch ( $booking->time_status() ) {
2166 case 'upcoming':
2167 $time_html = $booking->time_left;
2168 break;
2169 case 'now':
2170 $time_html = '<span class="time-left is-now">' . esc_html__( 'Now', 'latepoint' ) . '</span>';
2171 break;
2172 case 'past':
2173 $time_html = '<span class="time-left is-past">' . esc_html__( 'Past', 'latepoint' ) . '</span>';
2174 break;
2175 }
2176 return '<td><span class="in-table-time-left">' . $time_html . '</span></td>';
2177
2178 case 'agent':
2179 return '<td><div class="os-with-avatar">' .
2180 '<span class="cell-link-content">' .
2181 '<span class="os-avatar" style="background-image: url(' . esc_url( $booking->agent->get_avatar_url() ) . ')"></span>' .
2182 '<span class="os-name">' . esc_html( $booking->agent->full_name ) . '</span>' .
2183 '</span>' .
2184 '<div class="os-clickable-popup-trigger"' .
2185 ' data-route="' . esc_attr( OsRouterHelper::build_route_name( 'agents', 'mini_profile' ) ) . '"' .
2186 ' data-os-params="' . esc_attr(
2187 OsUtilHelper::build_os_params(
2188 [
2189 'agent_id' => $booking->agent_id,
2190 'booking_id' => $booking->id,
2191 ]
2192 )
2193 ) . '">' .
2194 '<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
2195 '</div>' .
2196 '</div></td>';
2197
2198 case 'location':
2199 return '<td>' . esc_html( $booking->location->name ) . '</td>';
2200
2201 case 'customer':
2202 return '<td><div class="os-with-avatar">' .
2203 '<span class="cell-link-content">' .
2204 '<span class="os-avatar" style="background-image: url(' . esc_url( $booking->customer->get_avatar_url() ) . ')"></span>' .
2205 '<span class="os-name">' . esc_html( $booking->customer->full_name ) . '</span>' .
2206 '</span>' .
2207 '<div class="os-clickable-popup-trigger"' .
2208 ' data-route="' . esc_attr( OsRouterHelper::build_route_name( 'customers', 'mini_profile' ) ) . '"' .
2209 ' data-os-params="' . esc_attr(
2210 OsUtilHelper::build_os_params(
2211 [
2212 'customer_id' => $booking->customer_id,
2213 'booking_id' => $booking->id,
2214 ]
2215 )
2216 ) . '">' .
2217 '<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
2218 '</div>' .
2219 '</div></td>';
2220
2221 case 'status':
2222 return '<td><span class="os-column-status os-column-status-' . esc_attr( $booking->status ) . '">' . esc_html( $booking->nice_status ) . '</span></td>';
2223
2224 case 'payment_status':
2225 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>';
2226
2227 case 'created_on':
2228 return '<td>' . esc_html( $booking->nice_created_at ) . '</td>';
2229
2230 default:
2231 // Extra columns: read from the appropriate model via property, getter, or meta.
2232 if ( 'extra' !== $col_def['type'] ) {
2233 return '<td></td>';
2234 }
2235 $extra_type = $col_def['extra_type'];
2236 $extra_key = $col_def['extra_key'];
2237 $model_obj = ( 'booking' === $extra_type ) ? $booking : $booking->$extra_type;
2238
2239 if ( property_exists( $model_obj, $extra_key ) || method_exists( $model_obj, 'get_' . $extra_key ) ) {
2240 return '<td>' . esc_html( $model_obj->$extra_key ) . '</td>';
2241 }
2242
2243 $column_value = $model_obj->get_meta_by_key( $extra_key );
2244 if ( $column_value &&
2245 ( $custom_fields_raw = OsSettingsHelper::get_settings_value( 'custom_fields_for_' . $extra_type, false ) ) &&
2246 ( $custom_fields_arr = json_decode( $custom_fields_raw, true ) ) &&
2247 isset( $custom_fields_arr[ $extra_key ]['type'] ) &&
2248 'multiselect' === $custom_fields_arr[ $extra_key ]['type']
2249 ) {
2250 $decoded = json_decode( $column_value );
2251 $column_value = is_array( $decoded ) ? implode( ', ', $decoded ) : $column_value;
2252 }
2253 return '<td>' . esc_html( $column_value ) . '</td>';
2254 }
2255 }
2256
2257 /**
2258 * Returns the <th> HTML for the bulk-selection column header (with the select-all checkbox).
2259 *
2260 * @return string
2261 */
2262 public static function render_bulk_select_header_cell(): string {
2263 return '<th class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
2264 '<label class="os-bulk-row-check-w">' .
2265 '<input type="checkbox" class="os-bulk-select-all" aria-label="' . esc_attr__( 'Select all appointments on this page', 'latepoint' ) . '" />' .
2266 '<span class="os-bulk-check-box"></span>' .
2267 '</label>' .
2268 '</th>';
2269 }
2270
2271 /**
2272 * Returns the empty spacer <th> HTML for the bulk-selection column in filter and footer rows.
2273 *
2274 * @return string
2275 */
2276 public static function render_bulk_select_spacer_cell(): string {
2277 return '<th class="os-bulk-select-cell"></th>';
2278 }
2279
2280 /**
2281 * Returns the <td> HTML for the bulk-selection column on a single body row.
2282 *
2283 * @param OsBookingModel $booking Current booking row.
2284 * @return string
2285 */
2286 public static function render_bulk_select_body_cell( OsBookingModel $booking ): string {
2287 /* translators: %d: appointment ID */
2288 $aria_label = sprintf( __( 'Select appointment %d', 'latepoint' ), $booking->id );
2289 return '<td class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
2290 '<label class="os-bulk-row-check-w">' .
2291 '<input type="checkbox" class="os-bulk-row-check" value="' . esc_attr( $booking->id ) . '" aria-label="' . esc_attr( $aria_label ) . '" />' .
2292 '<span class="os-bulk-check-box"></span>' .
2293 '</label>' .
2294 '</td>';
2295 }
2296
2297 /**
2298 * Returns the HTML for the bulk-actions toolbar shown above the appointments table.
2299 *
2300 * @return string
2301 */
2302 public static function render_bulk_actions_bar(): string {
2303 return '<div class="os-bulk-actions-bar" data-bulk-nonce="' . esc_attr( wp_create_nonce( 'bulk_destroy_bookings' ) ) . '">' .
2304 '<div class="os-bulk-actions-info">' .
2305 '<span class="os-bulk-selected-count">0</span>' .
2306 // Label is swapped between singular and plural by JS based on selection count.
2307 '<span class="os-bulk-selected-label">' . esc_html__( 'Appointments selected', 'latepoint' ) . '</span>' .
2308 '</div>' .
2309 '<div class="os-bulk-actions-controls">' .
2310 '<a href="#" class="latepoint-btn latepoint-btn-danger os-bulk-action-delete">' .
2311 '<i class="latepoint-icon latepoint-icon-trash-2"></i><span>' . esc_html__( 'Delete Selected', 'latepoint' ) . '</span>' .
2312 '</a>' .
2313 '<a href="#" class="os-bulk-actions-clear">' . esc_html__( 'Clear selection', 'latepoint' ) . '</a>' .
2314 '</div>' .
2315 '</div>';
2316 }
2317 }
2318