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