[Back]
<?php

class OsBookingHelper {


	/**
	 * @param OsBookingModel $booking
	 *
	 * @return mixed|void
	 *
	 * Returns full amount to charge in database format 1999.0000
	 *
	 */
	public static function calculate_full_amount_for_booking( OsBookingModel $booking ) {
		if ( ! $booking->service_id ) {
			return 0;
		}
		$amount = self::calculate_full_amount_for_service( $booking );
		$amount = apply_filters( 'latepoint_calculate_full_amount_for_booking', $amount, $booking );
		$amount = OsMoneyHelper::pad_to_db_format( $amount );

		return $amount;
	}


	/**
	 * @param OsBookingModel $booking
	 *
	 * @return mixed|void
	 *
	 */
	public static function calculate_full_amount_for_service( OsBookingModel $booking ) {
		if ( ! $booking->service_id ) {
			return 0;
		}
		$service            = new OsServiceModel( $booking->service_id );
		$amount_for_service = $service->get_full_amount_for_duration( $booking->duration );
		$amount_for_service = apply_filters( 'latepoint_full_amount_for_service', $amount_for_service, $booking );

		return $amount_for_service;
	}


	/**
	 * @param OsBookingModel $booking
	 *
	 * @return mixed|void
	 *
	 * Returns deposit amount to charge in database format 1999.0000
	 *
	 */
	public static function calculate_deposit_amount_to_charge( OsBookingModel $booking ) {
		if ( ! $booking->service_id ) {
			return 0;
		}
		$service            = new OsServiceModel( $booking->service_id );
		$amount = $service->get_deposit_amount_for_duration( $booking->duration );
		$amount = apply_filters( 'latepoint_deposit_amount_for_service', $amount, $booking );
		$amount             = OsMoneyHelper::pad_to_db_format( $amount );

		return $amount;
	}


	/**
	 * @param array $item_data
	 *
	 * @return OsBookingModel
	 */
	public static function build_booking_model_from_item_data( array $item_data ): OsBookingModel {
		$booking = new OsBookingModel();
		if ( $item_data['id'] ) {
			$booking = $booking->load_by_id( $item_data['id'] );
			if ( ! $booking ) {
				$booking = new OsBookingModel();
			}
		}
		$booking->set_data( $item_data );
		// get buffers from service and set to booking object
		if ( ! isset( $item_data['buffer_before'] ) && ! isset( $item_data['buffer_after'] ) ) {
			$booking->set_buffers();
		}
		if ( empty( $booking->end_time ) ) {
			$booking->calculate_end_date_and_time();
		}
		if ( empty( $booking->end_date ) ) {
			$booking->calculate_end_date();
		}
		$booking->set_utc_datetimes();

		return $booking;
	}

	public static function get_booking_id_and_manage_ability_by_key( string $key ) {
		$booking_id = OsMetaHelper::get_booking_id_by_meta_value( "key_to_manage_for_agent", $key );
		if ( $booking_id ) {
			return [ 'booking_id' => $booking_id, 'for' => 'agent' ];
		}

		$booking_id = OsMetaHelper::get_booking_id_by_meta_value( "key_to_manage_for_customer", $key );
		if ( $booking_id ) {
			return [ 'booking_id' => $booking_id, 'for' => 'customer' ];
		}

		return false;
	}

	public static function is_action_allowed( string $action, OsBookingModel $booking, string $key = '' ) {
		$is_allowed = false;
		if ( empty( $booking->id ) ) {
			return false;
		}
		if ( ! in_array( $action, [ 'cancel', 'reschedule' ] ) ) {
			return false;
		}
		$action_result = false;
		switch ( $action ) {
			case 'cancel':
				$action_result = OsCustomerHelper::can_cancel_booking( $booking );
				break;
			case 'reschedule':
				$action_result = OsCustomerHelper::can_reschedule_booking( $booking );
				break;
		}
		if ( ! empty( $key ) ) {
			// key is passed, check if allowed through key
			$agent_key_meta    = OsMetaHelper::get_booking_meta_by_key( 'key_to_manage_for_agent', $booking->id );
			$customer_key_meta = OsMetaHelper::get_booking_meta_by_key( 'key_to_manage_for_customer', $booking->id );
			if ( $key == $agent_key_meta ) {
				// agent can do everything, no need to check for action
				$is_allowed = true;
			} elseif ( $key == $customer_key_meta ) {
				// customer
				$is_allowed = $action_result;
			}
		} elseif ( OsAuthHelper::get_logged_in_customer_id() == $booking->customer_id ) {
			$is_allowed = $action_result;
		}

		return $is_allowed;
	}

	public static function generate_add_to_calendar_links( OsBookingModel $booking, $key = false ): string {
		$html = '<div class="add-to-calendar-types">
							<div class="atc-heading-wrapper">
								<div class="atc-heading">' . esc_html__( 'Calendar Type', 'latepoint' ) . '</div>
								<div class="close-calendar-types"></div>
							</div>
							<a href="' . esc_url($booking->get_ical_download_link( $key )) . '" target="_blank" class="atc-type atc-type-apple">
								<div class="atc-type-image"></div>
								<div class="atc-type-name">' . esc_html__( 'Apple Calendar', 'latepoint' ) . '</div>
							</a>
							<a href="' . esc_url($booking->get_url_for_add_to_calendar_button( 'google' )) . '" target="_blank" class="atc-type atc-type-google">
								<div class="atc-type-image"></div>
								<div class="atc-type-name">' . esc_html__( 'Google Calendar', 'latepoint' ) . '</div>
							</a>
							<a href="' . esc_url($booking->get_url_for_add_to_calendar_button( 'outlook' )) . '" target="_blank" class="atc-type atc-type-outlook">
								<div class="atc-type-image"></div>
								<div class="atc-type-name">' . esc_html__( 'Outlook.com', 'latepoint' ) . '</div>
							</a>
							<a href="' . esc_url($booking->get_url_for_add_to_calendar_button( 'outlook' )) . '" target="_blank" class="atc-type atc-type-office-365">
								<div class="atc-type-image"></div>
								<div class="atc-type-name">' . esc_html__( 'Microsoft 365', 'latepoint' ) . '</div>
							</a>
						</div>';

		return $html;
	}


	public static function get_bookings_for_select( $should_be_in_future = false ) {
		$bookings = new OsBookingModel();
		if ( $should_be_in_future ) {
			$bookings = $bookings->should_be_in_future();
		}
		$bookings         = $bookings->order_by( 'id desc' )->set_limit( 100 )->get_results_as_models();
		$bookings_options = [];
		foreach ( $bookings as $booking ) {
			$name               = $booking->service->name . ', ' . $booking->agent->full_name . ', ' . $booking->customer->full_name . ' [' . $booking->booking_code . ' : ' . $booking->id . ']';
			$bookings_options[] = [ 'value' => $booking->id, 'label' => esc_html( $name ) ];
		}

		return $bookings_options;
	}

	/**
	 *
	 * Determine whether to show
	 *
	 * @param $rows
	 *
	 * @return bool
	 */
	public static function is_breakdown_free( $rows ) {
		return ( ( empty( $rows['subtotal']['raw_value'] ) || ( (float) $rows['subtotal']['raw_value'] <= 0 ) ) && ( empty( $rows['total']['raw_value'] ) || ( (float) $rows['total']['raw_value'] <= 0 ) ) );
	}

	public static function output_price_breakdown( $rows ) {
		foreach ( $rows['before_subtotal'] as $row ) {
			self::output_price_breakdown_row( $row );
		}
		// if there is nothing between subtotal and total - don't show subtotal as it will be identical to total
		if ( ! empty( $rows['after_subtotal'] ) ) {
			if ( ! empty( $rows['subtotal'] ) ) {
				echo '<div class="subtotal-separator"></div>';
				self::output_price_breakdown_row( $rows['subtotal'] );
			}
			foreach ( $rows['after_subtotal'] as $row ) {
				self::output_price_breakdown_row( $row );
			}
		}
		if ( ! empty( $rows['total'] ) ) {
			self::output_price_breakdown_row( $rows['total'] );
		}
		if ( ! empty( $rows['payments'] ) ) {
			foreach ( $rows['payments'] as $row ) {
				self::output_price_breakdown_row( $row );
			}
		}
		if ( ! empty( $rows['balance'] ) ) {
			self::output_price_breakdown_row( $rows['balance'] );
		}
	}

	public static function output_price_breakdown_row( $row ) {
		if ( ! empty( $row['items'] ) ) {
			if ( ! empty( $row['heading'] ) ) {
				echo '<div class="summary-box-heading"><div class="sbh-item">' . esc_html($row['heading']) . '</div><div class="sbh-line"></div></div>';
			}
			foreach ( $row['items'] as $row_item ) {
				self::output_price_breakdown_row( $row_item );
			}
		} else {
			$extra_class = '';
			if ( isset( $row['style'] ) && $row['style'] == 'strong' ) {
				$extra_class .= ' spi-strong';
			}
			if ( isset( $row['style'] ) && $row['style'] == 'total' ) {
				$extra_class .= ' spi-total';
			}
			if ( isset( $row['type'] ) && $row['type'] == 'credit' ) {
				$extra_class .= ' spi-positive';
			}
			?>
            <div class="summary-price-item-w <?php echo esc_attr($extra_class); ?>">
                <div class="spi-name">
					<?php echo $row['label']; ?>
					<?php if ( ! empty( $row['note'] ) ) {
						echo '<span class="pi-note">' . esc_html($row['note']) . '</span>';
					} ?>
					<?php if ( ! empty( $row['badge'] ) ) {
						echo '<span class="pi-badge">' . esc_html($row['badge']) . '</span>';
					} ?>
                </div>
                <div class="spi-price"><?php echo esc_html($row['value']); ?></div>
            </div>
			<?php
		}
	}

	public static function output_price_breakdown_row_as_input_field( $row, $base_name ) {
		$field_name = $base_name . '[' . OsUtilHelper::random_text( 'alnum', 8 ) . ']';
		if ( ! empty( $row['items'] ) ) {
			echo OsFormHelper::hidden_field( $field_name . '[heading]', $row['heading'] ?? '' );
			foreach ( $row['items'] as $row_item ) {
				self::output_price_breakdown_row_as_input_field( $row_item, $field_name . '[items]' );
			}
		} else {
			$wrapper_class = ( $row['raw_value'] < 0 ) ? [ 'class' => 'green-value-input' ] : [];
			$label         = $row['label'] ?? '';
			if ( ! empty( $row['note'] ) ) {
				$label .= ' ' . $row['note'];
			}
			echo OsFormHelper::money_field( $field_name . '[value]', $label, $row['raw_value'], [ 'theme' => 'right-aligned' ], [], $wrapper_class );
			echo OsFormHelper::hidden_field( $field_name . '[label]', $row['label'] ?? '' );
			echo OsFormHelper::hidden_field( $field_name . '[style]', $row['style'] ?? '' );
			echo OsFormHelper::hidden_field( $field_name . '[type]', $row['type'] ?? '' );
			echo OsFormHelper::hidden_field( $field_name . '[note]', $row['note'] ?? '' );
			echo OsFormHelper::hidden_field( $field_name . '[badge]', $row['badge'] ?? '' );
		}
		if ( ! empty( $row['sub_items'] ) ) {
			foreach ( $row['sub_items'] as $row_item ) {
				self::output_price_breakdown_row_as_input_field( $row_item, $field_name . '[sub_items]' );
			}
		}
	}

	/**
	 * @param \LatePoint\Misc\Filter $filter
	 * @param bool $accessed_from_backend
	 *
	 * @return array
	 */
	public static function get_blocked_periods_grouped_by_day( \LatePoint\Misc\Filter $filter, bool $accessed_from_backend = false ): array {
		$grouped_blocked_periods = [];

		if ( $filter->date_from ) {
			$date_from = OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
			$date_to   = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );

			# Loop through days to fill in days that might have no bookings
			for ( $day = clone $date_from; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
				$grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] = [];
			}
		}
		if ( ! $accessed_from_backend ) {
			$today                     = new OsWpDateTime( 'today' );
            $earliest_possible_booking = OsSettingsHelper::get_earliest_possible_booking_restriction($filter->service_id);

			$block_end_datetime        = OsTimeHelper::now_datetime_object();
			if ( $earliest_possible_booking ) {
				try {
					$block_end_datetime->modify( $earliest_possible_booking );
				} catch ( Exception $e ) {
					$block_end_datetime = OsTimeHelper::now_datetime_object();
				}
			}
			for ( $day = clone $today; $day->format( 'Y-m-d' ) <= $block_end_datetime->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
				// loop days from now to the earliest possible booking and block timeslots if these days were actually requested
				if ( isset( $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] ) ) {
					$grouped_blocked_periods[ $day->format( 'Y-m-d' ) ][] = new \LatePoint\Misc\BlockedPeriod( [
						'start_time' => 0,
						'end_time'   => ( $day->format( 'Y-m-d' ) < $block_end_datetime->format( 'Y-m-d' ) ) ? 24 * 60 : OsTimeHelper::convert_datetime_to_minutes( $block_end_datetime ),
						'start_date' => $day->format( 'Y-m-d' ),
						'end_date'   => $day->format( 'Y-m-d' )
					] );
				}
			}
            $latest_possible_booking = OsSettingsHelper::get_latest_possible_booking_restriction($filter->service_id);
			if ( $latest_possible_booking ) {
				try {
					$latest_booking_datetime = OsTimeHelper::now_datetime_object();
					$latest_booking_datetime->modify( $latest_possible_booking );
				} catch ( Exception $e ) {
					$latest_booking_datetime = null;
				}
                if ( $latest_booking_datetime && $filter->date_from) {
                    $date_to   = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
                    // Start from the latest_booking_datetime day
                    for ( $day = clone $latest_booking_datetime; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
                        if ( isset( $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] ) ) {
	                        $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ][] = new \LatePoint\Misc\BlockedPeriod( [
		                        'start_time' => ( $day->format( 'Y-m-d' ) == $latest_booking_datetime->format( 'Y-m-d' ) ) ? OsTimeHelper::convert_datetime_to_minutes( $latest_booking_datetime ) : 0,
		                        'end_time'   => 24 * 60,
		                        'start_date' => $day->format( 'Y-m-d' ),
		                        'end_date'   => $day->format( 'Y-m-d' )
	                        ] );
                        }
                    }
                }
			}

		}

		$grouped_blocked_periods = apply_filters( 'latepoint_blocked_periods_for_range', $grouped_blocked_periods, $filter );

		return $grouped_blocked_periods;
	}

	/**
	 * @param \LatePoint\Misc\Filter $filter
	 *
	 * @return array
	 */
	public static function get_booked_periods_grouped_by_day( \LatePoint\Misc\Filter $filter ): array {
		$booked_periods = self::get_booked_periods( $filter );

		$grouped_booked_periods = [];
		if ( $filter->date_from ) {
			$date_from = OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
			$date_to   = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );

			# Loop through days to fill in days that might have no bookings
			for ( $day = clone $date_from; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
				$grouped_booked_periods[ $day->format( 'Y-m-d' ) ] = [];
			}
			foreach ( $booked_periods as $booked_period ) {
				$grouped_booked_periods[ $booked_period->start_date ][] = $booked_period;
				// if event spans multiple days - add to other days as well
				if ( $booked_period->end_date && ( $booked_period->start_date != $booked_period->end_date ) ) {
					$grouped_booked_periods[ $booked_period->end_date ][] = $booked_period;
				}
			}
		}

		return $grouped_booked_periods;
	}

	/**
	 * @param \LatePoint\Misc\Filter $filter
	 *
	 * @return \LatePoint\Misc\BookedPeriod[]
	 */
	public static function get_booked_periods( \LatePoint\Misc\Filter $filter ): array {


		$bookings       = self::get_bookings( $filter, true );
		$booked_periods = [];

		foreach ( $bookings as $booking ) {
			$booked_periods[] = \LatePoint\Misc\BookedPeriod::create_from_booking_model( $booking );
		}


        if($filter->consider_cart_items){
            $cart = OsCartsHelper::get_or_create_cart();
            $bookings_in_cart = $cart->get_bookings_from_cart_items();

            foreach ( $bookings_in_cart as $cart_booking ) {
                $booked_periods[] = \LatePoint\Misc\BookedPeriod::create_from_booking_model( $cart_booking );
            }
        }

		// TODO Update all filters to accept new "filter" variable (In Google Calendar addon)
		$booked_periods = apply_filters( 'latepoint_get_booked_periods', $booked_periods, $filter );

		return $booked_periods;
	}


	/**
	 * @param \LatePoint\Misc\Filter $filter
	 * @param bool $as_models
	 *
	 * @return array
	 */
	public static function get_bookings( \LatePoint\Misc\Filter $filter, bool $as_models = false ): array {
		$bookings = new OsBookingModel();
		if ( $filter->date_from ) {
			if ( $filter->date_from && $filter->date_to ) {
				# both start and end date provided - means it's a range
				$bookings->where( [ 'start_date >=' => $filter->date_from, 'start_date <=' => $filter->date_to ] );
			} else {
				# only start_date provided - means it's a specific date requested
				$bookings->where( [ 'start_date' => $filter->date_from ] );
			}
		}


		if ( $filter->connections ) {
			$connection_conditions = [];
			foreach ( $filter->connections as $connection ) {
				$connection_conditions[] = [
					'AND' =>
						[
							'agent_id'    => $connection->agent_id,
							'service_id'  => $connection->service_id,
							'location_id' => $connection->location_id
						]
				];
			}
			$bookings->where( [ 'OR' => $connection_conditions ] );
		} else {
			if ( $filter->agent_id ) {
				$bookings->where( [ 'agent_id' => $filter->agent_id ] );
			}
			if ( $filter->location_id ) {
				$bookings->where( [ 'location_id' => $filter->location_id ] );
			}
			if ( $filter->service_id ) {
				$bookings->where( [ 'service_id' => $filter->service_id ] );
			}
		}
		if ( $filter->statuses ) {
			$bookings->where( [ 'status' => $filter->statuses ] );
		}
		if ( $filter->exclude_booking_ids ) {
			$bookings->where( [ 'id NOT IN' => $filter->exclude_booking_ids ] );
		}
		$bookings->order_by( 'start_time asc, end_time asc, service_id asc' );
		$bookings = ( $as_models ) ? $bookings->get_results_as_models() : $bookings->get_results();

		// make sure to return empty array if nothing is found
		if ( empty( $bookings ) ) {
			$bookings = [];
		}

		return $bookings;
	}

	public static function generate_ical_event_string( $booking ) {
        // translators: %1$s is agent name, %2$s is service name
		$booking_description = sprintf( __( 'Appointment with %1$s for %2$s', 'latepoint' ), $booking->agent->full_name, $booking->service->name );

		$ics = new ICS( array(
			'location'    => $booking->location->full_address,
			'description' => '',
			'dtstart'     => $booking->format_start_date_and_time_for_google(),
			'dtend'       => $booking->format_end_date_and_time_for_google(),
			'summary'     => $booking_description,
			'url'         => get_site_url()
		) );

		return $ics->to_string();
	}

	/**
	 * @param \LatePoint\Misc\BookingRequest $booking_request
	 *
	 * @return bool
	 *
	 * Checks if requested booking slot is available, loads work periods and booked periods from database and checks availability against them
	 */
	public static function is_booking_request_available( \LatePoint\Misc\BookingRequest $booking_request, $settings = [] ): bool {
        try{
            $requested_date = new OsWpDateTime( $booking_request->start_date );
        }catch(Exception $e){
            return false;
        }
		$resources      = OsResourceHelper::get_resources_grouped_by_day( $booking_request, $requested_date, $requested_date, $settings );
		if ( empty( $resources[ $requested_date->format( 'Y-m-d' ) ] ) ) {
			return false;
		}
		$is_available = false;


        // check if satisfies earliest and latest bookings - check per-service settings first, then global
        $earliest_possible_booking = OsSettingsHelper::get_earliest_possible_booking_restriction($booking_request->service_id);
        $latest_possible_booking = OsSettingsHelper::get_latest_possible_booking_restriction($booking_request->service_id);


        if($earliest_possible_booking || $latest_possible_booking){
            // check earliest
            if(!empty($earliest_possible_booking)) {
	            try {
		            $earliest_possible_booking_date = new OsWpDateTime( $earliest_possible_booking );
		            if ( $earliest_possible_booking_date > $booking_request->get_start_datetime() ) {
			            return false;
		            }
	            } catch ( Exception $e ) {

	            }
            }
            if(!empty($latest_possible_booking)) {
	            // check latest
	            try {
		            $latest_possible_booking_date = new OsWpDateTime( $latest_possible_booking );
		            if ( $latest_possible_booking_date < $booking_request->get_start_datetime() ) {
			            return false;
		            }
	            } catch ( Exception $e ) {

	            }
            }
        }

		foreach ( $resources[ $requested_date->format( 'Y-m-d' ) ] as $resource ) {
			foreach ( $resource->slots as $slot ) {
				if ( $slot->start_time == $booking_request->start_time && $slot->can_accomodate( $booking_request->total_attendees ) ) {
					$is_available = true;
				}
				if ( $is_available ) {
					break;
				}
			}
			if ( $is_available ) {
				break;
			}
		}

		return $is_available;
	}

	/**
	 *
	 * Checks if two bookings are part of the same group appointment
	 *
	 * @param bool|OsBookingModel $booking
	 * @param bool|OsBookingModel $compare_booking
	 *
	 * @return bool
	 */
	public static function check_if_group_bookings( $booking, $compare_booking ): bool {
		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 ) ) {
			return true;
		} else {
			return false;
		}
	}


	public static function process_actions_after_save( $booking_id ) {
	}

	/**
	 * @param DateTime $start_date
	 * @param DateTime $end_date
	 * @param \LatePoint\Misc\BookingRequest $booking_request
	 * @param \LatePoint\Misc\BookingResource[] $resources
	 * @param array $settings
	 *
	 * @return string
	 * @throws Exception
	 */
	public static function get_quick_availability_days( DateTime $start_date, DateTime $end_date, \LatePoint\Misc\BookingRequest $booking_request, array $resources = [], array $settings = [] ) {
		$default_settings = [
			'work_boundaries'     => false,
			'exclude_booking_ids' => []
		];
		$settings         = array_merge( $default_settings, $settings );

		$html = '';

		if ( ! $resources ) {
			$resources = OsResourceHelper::get_resources_grouped_by_day( $booking_request, $start_date, $end_date, $settings );
		}
		if ( ! $settings['work_boundaries'] ) {
			$settings['work_boundaries'] = OsResourceHelper::get_work_boundaries_for_groups_of_resources( $resources );
		}

        if ( $start_date->format( 'j' ) != '1' ) {
            $html .= '<div class="ma-month-label">' . OsUtilHelper::get_month_name_by_number( $start_date->format( 'n' ) ) . '</div>';
        }

		for ( $day_date = clone $start_date; $day_date <= $end_date; $day_date->modify( '+1 day' ) ) {
			// first day of month, output month name
			if ( $day_date->format( 'j' ) == '1' ) {
				$html .= '<div class="ma-month-label">' . OsUtilHelper::get_month_name_by_number( $day_date->format( 'n' ) ) . '</div>';
			}
			$html .= '<div class="ma-day ma-day-number-' . $day_date->format( 'N' ) . '">';
			$html .= '<div class="ma-day-info">';
			$html .= '<span class="ma-day-number">' . $day_date->format( 'j' ) . '</span>';
			$html .= '<span class="ma-day-weekday">' . OsUtilHelper::get_weekday_name_by_number( $day_date->format( 'N' ), true ) . '</span>';
			$html .= '</div>';
			$html .= OsTimelineHelper::availability_timeline( $booking_request, $settings['work_boundaries'], $resources[ $day_date->format( 'Y-m-d' ) ], [ 'book_on_click' => false ] );
			$html .= '</div>';
		}

		return $html;
	}

	public static function count_pending_bookings() {
		$bookings = new OsBookingModel();

		return $bookings->filter_allowed_records()->where( [ 'status IN' => OsBookingHelper::get_booking_statuses_for_pending_page() ] )->count();
	}


	public static function generate_bundles_folder(): void {
		$bundles_model = new OsBundleModel();
		$bundles       = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();

		if ( $bundles ) {
			?>
            <div class="os-item-category-w os-items os-as-rows os-animated-child">
                <div class="os-item-category-info-w os-item os-animated-self with-plus">
                    <div class="os-item-category-info os-item-i" tabindex="0">
                        <div class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></div>
                        <div class="os-item-name-w">
                            <div class="os-item-name"><?php echo esc_html__( 'Bundle & Save', 'latepoint' ); ?></div>
                        </div>
						<?php if (OsSettingsHelper::is_on('show_service_categories_count') && count( $bundles ) ) { ?>
                            <div class="os-item-child-count">
                                <span><?php echo count( $bundles ); ?></span> <?php esc_html_e( 'Bundles', 'latepoint' ); ?>
                            </div>
						<?php } ?>
                    </div>
                </div>
                <div class="os-bundles os-animated-parent os-items os-as-rows os-selectable-items">
					<?php
					foreach ( $bundles as $bundle ) { ?>
                        <div class="os-animated-child os-item os-selectable-item <?php echo ( $bundle->charge_amount ) ? 'os-priced-item' : ''; ?> <?php if ( $bundle->short_description ) { echo 'with-description'; } ?>"
                                tabindex="0"
                                data-item-price="<?php echo esc_attr($bundle->charge_amount); ?>"
                                data-priced-item-type="bundle"
                                data-summary-field-name="bundle"
                                data-summary-value="<?php echo esc_attr( $bundle->name ); ?>"
                                data-item-id="<?php echo esc_attr($bundle->id); ?>"
                                data-cart-item-item-data-key="bundle_id"
                                data-os-call-func="latepoint_bundle_selected">
                            <div class="os-service-selector os-item-i os-animated-self"
                                 data-bundle-id="<?php echo esc_attr($bundle->id); ?>">
                                <span class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></span>
                                <span class="os-item-name-w">
		                <span class="os-item-name"><?php echo esc_html($bundle->name); ?></span>
		                <?php if ( $bundle->short_description ) { ?>
                            <span class="os-item-desc"><?php echo wp_kses_post($bundle->short_description); ?></span>
		                <?php } ?>
		              </span>

							<?php if ( $bundle->charge_amount > 0 ) { ?>
                                <span class="os-item-price-w">
                  <span class="os-item-price">
                    <?php echo esc_html(OsMoneyHelper::format_price($bundle->charge_amount)); ?>
                  </span>
                </span>
							<?php } ?>
                            </div>
                        </div>
						<?php
					}
					?>
                </div>
            </div>
			<?php
		}
	}

	public static function generate_services_list( $services = false, $preselected_service = false ) {
		if ( $services && is_array( $services ) && ! empty( $services ) ) { ?>
            <div class="os-services os-animated-parent os-items os-as-rows os-selectable-items">
				<?php foreach ( $services as $service ) {
					// if service is preselected - only output that service, skip the rest
					if ( $preselected_service && $service->id != $preselected_service->id ) {
						continue;
					}
					$service_durations = $service->get_all_durations_arr();
					$is_priced         = ( ! ( count( $service_durations ) > 1 ) && $service->charge_amount ) ? true : false;
					?>
                    <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 ) { echo 'with-description'; } ?>"
                            tabindex="0"
                            data-item-price="<?php echo esc_attr($service->charge_amount); ?>"
                            data-priced-item-type="service"
                            data-summary-field-name="service"
                            data-summary-value="<?php echo esc_attr( $service->name ); ?>"
                            data-item-id="<?php echo esc_attr($service->id); ?>"
                            data-cart-item-item-data-key="service_id"
                            data-os-call-func="latepoint_service_selected"
                            data-id-holder=".latepoint_service_id">
                        <div class="os-service-selector os-item-i os-animated-self" data-service-id="<?php echo esc_attr($service->id); ?>">
							<?php if ( $service->selection_image_id ) { ?>
                                <span class="os-item-img-w" style="background-image: url(<?php echo esc_url($service->selection_image_url); ?>);"></span>
							<?php } ?>
                            <span class="os-item-name-w">
                <span class="os-item-name"><?php echo esc_html($service->name); ?></span>
                <?php if ( $service->short_description ) { ?>
                    <span class="os-item-desc"><?php echo wp_kses_post($service->short_description); ?></span>
                <?php } ?>
              </span>
							<?php if ( $service->price_min > 0 ) { ?>
                                <span class="os-item-price-w">
                  <span class="os-item-price">
                    <?php
                    /**
                     * Filters the display price value shown on the service tile on a booking form
                     *
                     * @since 5.1.94
                     * @hook latepoint_booking_form_display_service_price
                     *
                     * @param {string} $price displayed price that will be outputted
                     * @param {OsServiceModel} $service Service that the price is displayed for
                     *
                     * @returns {string} Filtered displayed price
                     */
                    $display_price = apply_filters('latepoint_booking_form_display_service_price', $service->price_min_formatted, $service);
                      echo esc_html($display_price) ?>
                  </span>
                  <?php if ( $service->price_min != $service->price_max ) { ?>
                      <span class="os-item-price-label"><?php esc_html_e( 'Starts From', 'latepoint' ); ?></span>
                  <?php } ?>
                </span>
							<?php } ?>
                        </div>
                    </div>
				<?php } ?>
            </div>
		<?php }
	}

	public static function generate_services_bundles_and_categories_list( $parent_id = false, array $settings = [] ) {
		$default_settings = [
			'show_service_categories_arr' => false,
			'show_services_arr'           => false,
			'preselected_service'         => false,
			'preselected_category'        => false,
		];
		$settings         = array_merge( $default_settings, $settings );

		if ( $settings['preselected_service'] ) {
			OsBookingHelper::generate_services_list( [ $settings['preselected_service'] ], $settings['preselected_service'] );

			return;
		}

		$service_categories = new OsServiceCategoryModel();
		$args               = array();
		if ( $settings['show_service_categories_arr'] && is_array( $settings['show_service_categories_arr'] ) ) {
			if ( $parent_id ) {
				$service_categories->where( [ 'parent_id' => $parent_id ] );
			} else {
				if ( $settings['preselected_category'] ) {
					$service_categories->where( [ 'id' => $settings['preselected_category'] ] );
				} else {
					$service_categories->where_in( 'id', $settings['show_service_categories_arr'] );
					$service_categories->where( [
						'parent_id' => [
							'OR' => [
								'IS NULL',
								' NOT IN' => $settings['show_service_categories_arr']
							]
						]
					] );
				}
			}
		} else {
			if ( $settings['preselected_category'] ) {
				$service_categories->where( [ 'id' => $settings['preselected_category'] ] );
			} else {
				$args['parent_id'] = $parent_id ? $parent_id : 'IS NULL';
			}
		}
		$service_categories = $service_categories->where( $args )->order_by( 'order_number asc' )->get_results_as_models();

		$main_parent_class = ( $parent_id ) ? 'os-animated-parent' : 'os-item-categories-main-parent os-animated-parent';
		if ( ! $settings['preselected_category'] ) {
			echo '<div class="os-item-categories-holder ' . esc_attr($main_parent_class) . '">';
		}

		// generate services that have no category
		if ( $parent_id == false && $settings['preselected_category'] == false ) { ?>
			<?php
			$services_without_category = new OsServiceModel();
			if ( $settings['show_services_arr'] ) {
				$services_without_category->where_in( 'id', $settings['show_services_arr'] );
			}
			$services_without_category = $services_without_category->where( [ 'category_id' => 0 ] )->should_be_active()->get_results_as_models();
			if ( $services_without_category ) {
				OsBookingHelper::generate_services_list( $services_without_category, false );
			}
		}

		if ( is_array( $service_categories ) ) {
			foreach ( $service_categories as $service_category ) { ?>
				<?php
				$services          = [];
				$category_services = $service_category->get_active_services();
				if ( is_array( $category_services ) ) {
					// if show selected services restriction is set - filter
					if ( $settings['show_services_arr'] ) {
						foreach ( $category_services as $category_service ) {
							if ( in_array( $category_service->id, $settings['show_services_arr'] ) ) {
								$services[] = $category_service;
							}
						}
					} else {
						$services = $category_services;
					}
				}
				$child_categories       = new OsServiceCategoryModel();
				$count_child_categories = $child_categories->where( [ 'parent_id' => $service_category->id ] )->count();
				// show only if it has either at least one child category or service
				if ( $count_child_categories || count( $services ) ) {
					// preselected category, just show contents, not the wrapper
					if ( $service_category->id == $settings['preselected_category'] ) {
						OsBookingHelper::generate_services_list( $services, false );
						OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) );
					} else { ?>
                    <div class="os-item-category-w os-items os-as-rows os-animated-child"
                         data-id="<?php echo esc_attr($service_category->id); ?>">
                        <div class="os-item-category-info-w os-item os-animated-self with-plus">
                            <div class="os-item-category-info os-item-i">
                                <div class="os-item-img-w"
                                     style="background-image: url(<?php echo esc_url($service_category->selection_image_url); ?>);"></div>
                                <div class="os-item-name-w">
                                    <div class="os-item-name"><?php echo esc_html($service_category->name); ?></div>
                                    <?php if (!empty($service_category->short_description)) { ?>
                                        <div class="os-item-desc"><?php echo $service_category->short_description; ?></div>
                                    <?php } ?>
                                </div>
								<?php if (OsSettingsHelper::is_on('show_service_categories_count') && count( $services ) ) { ?>
                                    <div class="os-item-child-count">
                                        <span><?php echo count( $services ); ?></span> <?php esc_html_e( 'Services', 'latepoint' ); ?>
                                    </div>
								<?php } ?>
                            </div>
                        </div>
						<?php OsBookingHelper::generate_services_list( $services, false ); ?>
						<?php OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) ); ?>
                        </div><?php
					}
				}
			}
		}
		if ( ! $settings['preselected_category'] && ! $parent_id ) {
			OsBookingHelper::generate_bundles_folder();
		}
		if ( ! $settings['preselected_category'] ) {
			echo '</div>';
		}
	}

	public static function group_booking_btn_html( $booking_id = false ) {
		$html = 'data-os-params="' . esc_attr(http_build_query( [ 'booking_id' => $booking_id ] )) . '" 
                  data-os-action="' . esc_attr(OsRouterHelper::build_route_name( 'bookings', 'grouped_bookings_quick_view' )) . '" 
                  data-os-output-target="lightbox"
                  data-os-lightbox-classes="width-500"
                  data-os-after-call="latepoint_init_grouped_bookings_form"';

		return $html;
	}

	public static function quick_booking_btn_html( $booking_id = false, $params = array() ) {
		$html = '';
		if ( $booking_id ) {
			$params['booking_id'] = $booking_id;
		}
		$route = OsRouterHelper::build_route_name( 'orders', 'quick_edit' );

		$params_str = http_build_query( $params );
		$html       = 'data-os-params="' . esc_attr($params_str) . '" 
    data-os-action="' . esc_attr($route) . '" 
    data-os-output-target="side-panel"
    data-os-after-call="latepoint_init_quick_order_form"';

		return $html;
	}


	/**
	 * @param OsBookingModel $booking
	 *
	 * @return false|mixed
	 *
	 * Search for available location based on booking requirements. Will return false if no available location found.
	 */
	public static function get_any_location_for_booking_by_rule( OsBookingModel $booking ) {
		// ANY LOCATION SELECTED
		// get available locations
		$connected_ids = OsLocationHelper::get_location_ids_for_service_and_agent( $booking->service_id, $booking->agent_id );

		// If date/time is selected - filter locations who are available at that time
		if ( $booking->start_date && $booking->start_time ) {
			$available_location_ids = [];
			$booking_request        = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
			foreach ( $connected_ids as $location_id ) {
				$booking_request->location_id = $location_id;
				if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
					$available_location_ids[] = $location_id;
				}
			}
			$connected_ids = array_intersect( $available_location_ids, $connected_ids );
		}


		$locations_model = new OsLocationModel();
		if ( ! empty( $connected_ids ) ) {
			$locations_model->where_in( 'id', $connected_ids );
			$locations = $locations_model->should_be_active()->get_results_as_models();
		} else {
			$locations = [];
		}

		if ( empty( $locations ) ) {
			return false;
		}

		$selected_location_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
		$booking->location_id = $selected_location_id;

		return $selected_location_id;
	}

	/**
	 * @param OsBookingModel $booking
	 *
	 * @return false|mixed
	 *
	 * Search for available agent based on booking requirements and agent picking preferences. Will return false if no available agent found.
	 */
	public static function get_any_agent_for_booking_by_rule( OsBookingModel $booking ) {
		// ANY AGENT SELECTED
		// get available agents
		$connected_ids = OsAgentHelper::get_agent_ids_for_service_and_location( $booking->service_id, $booking->location_id );

		// If date/time is selected - filter agents who are available at that time
		if ( $booking->start_date && $booking->start_time ) {
			$available_agent_ids = [];
			$booking_request     = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
			foreach ( $connected_ids as $agent_id ) {
				$booking_request->agent_id = $agent_id;
				if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
					$available_agent_ids[] = $agent_id;
				}
			}
			$connected_ids = array_intersect( $available_agent_ids, $connected_ids );
		}


		/**
		 * Get IDs of agents that are eligible to be assigned a booking that has "ANY" agent pre-selected
		 *
		 * @param {array} $connected_ids Array of eligible Agent IDs
		 * @param {OsBookingModel} $booking Booking that needs agent ID
		 *
		 * @returns {array} Filtered array of IDs of eligible agents
		 * @since 4.7.6
		 * @hook latepoint_agent_ids_assignable_to_any_agent_booking
		 *
		 */
		$connected_ids = apply_filters( 'latepoint_agent_ids_assignable_to_any_agent_booking', $connected_ids, $booking );

		if ( ! empty( $connected_ids ) ) {
			$agents_model = new OsAgentModel();
			$agents_model->where_in( 'id', $connected_ids );
			$agents = $agents_model->should_be_active()->get_results_as_models();
		} else {
			$agents = [];
		}

		if ( empty( $agents ) ) {
			return false;
		}


		$selected_agent_id = false;
		$agent_order_rule  = OsSettingsHelper::get_any_agent_order();
		switch ( $agent_order_rule ) {
			case LATEPOINT_ANY_AGENT_ORDER_RANDOM:
				$selected_agent_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
				break;
			case LATEPOINT_ANY_AGENT_ORDER_PRICE_HIGH:
				$highest_price = false;
				foreach ( $agents as $agent ) {
					$booking->agent_id = $agent->id;
					$price             = OsBookingHelper::calculate_full_amount_for_booking( $booking );
					if ( $highest_price === false && $selected_agent_id === false ) {
						$highest_price     = $price;
						$selected_agent_id = $agent->id;
					} else {
						if ( $highest_price < $price ) {
							$highest_price     = $price;
							$selected_agent_id = $agent->id;
						}
					}
				}
				break;
			case LATEPOINT_ANY_AGENT_ORDER_PRICE_LOW:
				$lowest_price = false;
				foreach ( $agents as $agent ) {
					$booking->agent_id = $agent->id;
					$price             = OsBookingHelper::calculate_full_amount_for_booking( $booking );
					if ( $lowest_price === false && $selected_agent_id === false ) {
						$lowest_price      = $price;
						$selected_agent_id = $agent->id;
					} else {
						if ( $lowest_price > $price ) {
							$lowest_price      = $price;
							$selected_agent_id = $agent->id;
						}
					}
				}
				break;
			case LATEPOINT_ANY_AGENT_ORDER_BUSY_HIGH:
				$max_bookings = false;
				foreach ( $agents as $agent ) {
					$agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
					if ( $max_bookings === false && $selected_agent_id === false ) {
						$max_bookings      = $agent_total_bookings;
						$selected_agent_id = $agent->id;
					} else {
						if ( $max_bookings < $agent_total_bookings ) {
							$max_bookings      = $agent_total_bookings;
							$selected_agent_id = $agent->id;
						}
					}
				}
				break;
			case LATEPOINT_ANY_AGENT_ORDER_BUSY_LOW:
				$min_bookings = false;
				foreach ( $agents as $agent ) {
					$agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
					if ( $min_bookings === false && $selected_agent_id === false ) {
						$min_bookings      = $agent_total_bookings;
						$selected_agent_id = $agent->id;
					} else {
						if ( $min_bookings > $agent_total_bookings ) {
							$min_bookings      = $agent_total_bookings;
							$selected_agent_id = $agent->id;
						}
					}
				}
				break;
		}
		/**
		 * Get ID of agent that will be assigned to a booking, depending on order rules, where agent is set to ANY
		 *
		 * @param {integer} $selected_agent_id Currently selected agent ID
		 * @param {OsAgentModel[]} $agents Array of eligible agent models to pick from
		 * @param {OsBookingModel} $booking Booking that needs agent ID
		 * @param {string} $agent_order_rule Rule of agent ordering
		 *
		 * @returns {integer} ID of the agent that will be assigned to this booking
		 * @since 4.7.6
		 * @hook latepoint_get_any_agent_id_for_booking_by_rule
		 *
		 */
		$selected_agent_id = apply_filters( 'latepoint_get_any_agent_id_for_booking_by_rule', $selected_agent_id, $agents, $booking, $agent_order_rule );
		$booking->agent_id = $selected_agent_id;

		return $selected_agent_id;
	}


	public static function get_total_bookings_for_date( $date, $conditions = [], $grouped = false ) {
		$args = [ 'start_date' => $date ];
		if ( isset( $conditions['agent_id'] ) && $conditions['agent_id'] ) {
			$args['agent_id'] = $conditions['agent_id'];
		}
		if ( isset( $conditions['service_id'] ) && $conditions['service_id'] ) {
			$args['service_id'] = $conditions['service_id'];
		}
		if ( isset( $conditions['location_id'] ) && $conditions['location_id'] ) {
			$args['location_id'] = $conditions['location_id'];
		}


		$bookings = new OsBookingModel();
		if ( $grouped ) {
			$bookings->group_by( 'start_date, start_time, end_time, service_id, location_id' );
		}
		$bookings = $bookings->where( $args );

		return $bookings->count();
	}


	/**
	 *
	 * Get list of statuses that block timeslot availability
	 *
	 * @return array
	 */
	public static function get_timeslot_blocking_statuses(): array {
		$statuses = explode( ',', OsSettingsHelper::get_settings_value( 'timeslot_blocking_statuses', '' ) );

		/**
		 * Get list of statuses that block timeslot availability
		 *
		 * @param {array} $statuses array of status codes that block timeslot availability
		 * @returns {array} The filtered array of status codes
		 *
		 * @since 4.7.0
		 * @hook latepoint_get_timeslot_blocking_statuses
		 *
		 */
		return apply_filters( 'latepoint_get_timeslot_blocking_statuses', $statuses );
	}


	/**
	 *
	 * Get list of statuses that appear on pending page
	 *
	 * @return array
	 */
	public static function get_booking_statuses_for_pending_page(): array {
		$statuses = explode( ',', OsSettingsHelper::get_settings_value( 'need_action_statuses', '' ) );

		/**
		 * Get list of statuses that appear on pending page
		 *
		 * @param {array} $statuses array of status codes that appear on pending page
		 * @returns {array} The filtered array of status codes
		 *
		 * @since 4.7.0
		 * @hook latepoint_get_booking_statuses_for_pending_page
		 *
		 */
		return apply_filters( 'latepoint_get_booking_statuses_for_pending_page', $statuses );
	}

	/**
	 *
	 * Get list of statuses that are not cancelled
	 *
	 * @return array
	 */
	public static function get_non_cancelled_booking_statuses(): array {
		$statuses = self::get_statuses_list();
        if(isset($statuses[LATEPOINT_BOOKING_STATUS_CANCELLED])) unset($statuses[LATEPOINT_BOOKING_STATUS_CANCELLED]);
        $statuses = array_keys($statuses);

		/**
		 * Get list of statuses that are not cancelled
		 *
		 * @param {array} $statuses array of status codes that are not cancelled
		 * @returns {array} The filtered array of status codes
		 *
		 * @since 5.0.5
		 * @hook get_non_cancelled_booking_statuses
		 *
		 */
		return apply_filters( 'get_non_cancelled_booking_statuses', $statuses );
	}


	public static function get_default_booking_status( $service_id = false ) {
		if ( $service_id ) {
			$service = new OsServiceModel( $service_id );
			if ( $service && ! empty( $service->id ) ) {
				return $service->get_default_booking_status();
			}
		}
		$default_status = OsSettingsHelper::get_settings_value( 'default_booking_status' );
		if ( $default_status ) {
			return $default_status;
		} else {
			return LATEPOINT_BOOKING_STATUS_APPROVED;
		}
	}


	public static function change_booking_status( $booking_id, $new_status ) {
		$booking = new OsBookingModel( $booking_id );
		if ( ! $booking_id || ! $booking ) {
			return false;
		}

		if ( $new_status == $booking->status ) {
			return true;
		} else {
			$old_booking = clone $booking;
			if ( $booking->update_status( $new_status ) ) {
				do_action( 'latepoint_booking_updated', $booking, $old_booking );

				return true;
			} else {
				return false;
			}
		}
	}


	/**
	 * @param \LatePoint\Misc\BookingRequest $booking_request
	 * @param \LatePoint\Misc\BookedPeriod[]
	 * @param int $capacity
	 *
	 * @return bool
	 */
	public static function is_timeframe_in_booked_periods( \LatePoint\Misc\BookingRequest $booking_request, array $booked_periods, OsServiceModel $service ): bool {
		if ( empty( $booked_periods ) ) {
			return false;
		}
		$count_existing_attendees = 0;
		foreach ( $booked_periods as $period ) {
			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() ) ) {
				// if it's the same service overlapping - count how many times
				// TODO maybe add an option to toggle on/off ability to share a timeslot capacity between two different services
				if ( $booking_request->service_id == $period->service_id ) {
					$count_existing_attendees += $period->total_attendees;
				} else {
					return true;
				}
			}
		}
		if ( $count_existing_attendees > 0 ) {
			// 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
			if ( ( $count_existing_attendees + $booking_request->total_attendees ) <= $service->get_capacity_needed_before_slot_is_blocked() ) {
				return false;
			} else {
				return true;
			}
		} else {
			// no attendees in the overlapping booked periods yet, just check if the requested number of attendees is within the service capacity
			if ( $booking_request->total_attendees <= $service->capacity_max ) {
				return false;
			} else {
				return true;
			}
		}
	}


	public static function is_period_overlapping( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
		// https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap/
		return ( ( $period_one_start < $period_two_end ) && ( $period_two_start < $period_one_end ) );
	}

	public static function is_period_inside_another( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
		return ( ( $period_one_start >= $period_two_start ) && ( $period_one_end <= $period_two_end ) );
	}

	// args = [agent_id, 'service_id', 'location_id']
	public static function get_bookings_for_date( $date, $args = [] ) {
		$bookings           = new OsBookingModel();
		$args['start_date'] = $date;
		// if any of these are false or 0 - remove it from arguments list
		if ( isset( $args['location_id'] ) && empty( $args['location_id'] ) ) {
			unset( $args['location_id'] );
		}
		if ( isset( $args['agent_id'] ) && empty( $args['agent_id'] ) ) {
			unset( $args['agent_id'] );
		}
		if ( isset( $args['service_id'] ) && empty( $args['service_id'] ) ) {
			unset( $args['service_id'] );
		}

		$bookings->where( $args )->order_by( 'start_time asc, end_time asc, service_id asc' );

		return $bookings->get_results_as_models();
	}

	/**
	 * @param \LatePoint\Misc\Filter $filter
	 *
	 * @return int
	 */
	public static function count_bookings( \LatePoint\Misc\Filter $filter ) {
		$bookings   = new OsBookingModel();
		$query_args = [];
		if ( $filter->date_from ) {
			$query_args['start_date'] = $filter->date_from;
		}
		if ( $filter->location_id ) {
			$query_args['location_id'] = $filter->location_id;
		}
		if ( $filter->agent_id ) {
			$query_args['agent_id'] = $filter->agent_id;
		}
		if ( $filter->service_id ) {
			$query_args['service_id'] = $filter->service_id;
		}

		return $bookings->should_not_be_cancelled()->where( $query_args )->count();
	}


	public static function get_nice_status_name( $status ) {
		$statuses_list = OsBookingHelper::get_statuses_list();
		if ( $status && isset( $statuses_list[ $status ] ) ) {
			return $statuses_list[ $status ];
		} else {
			return __( 'Undefined Status', 'latepoint' );
		}
	}


	public static function get_statuses_list() {
		$statuses            = [
			LATEPOINT_BOOKING_STATUS_APPROVED  => __( 'Approved', 'latepoint' ),
			LATEPOINT_BOOKING_STATUS_PENDING   => __( 'Pending Approval', 'latepoint' ),
			LATEPOINT_BOOKING_STATUS_CANCELLED => __( 'Cancelled', 'latepoint' ),
			LATEPOINT_BOOKING_STATUS_NO_SHOW   => __( 'No Show', 'latepoint' ),
			LATEPOINT_BOOKING_STATUS_COMPLETED => __( 'Completed', 'latepoint' ),
		];
		$additional_statuses = array_map( 'trim', explode( ',', OsSettingsHelper::get_settings_value( 'additional_booking_statuses', '' ) ) );
		if ( ! empty( $additional_statuses ) ) {
			foreach ( $additional_statuses as $status ) {
				if ( ! empty( $status ) ) {
					$statuses[ str_replace( ' ', '_', strtolower( $status ) ) ] = $status;
				}
			}
		}
		$statuses = apply_filters( 'latepoint_booking_statuses', $statuses );

		return $statuses;
	}


	public static function get_weekdays_arr( $full_name = false ) {
		if ( $full_name ) {
			$weekdays = array(
				__( 'Monday', 'latepoint' ),
				__( 'Tuesday', 'latepoint' ),
				__( 'Wednesday', 'latepoint' ),
				__( 'Thursday', 'latepoint' ),
				__( 'Friday', 'latepoint' ),
				__( 'Saturday', 'latepoint' ),
				__( 'Sunday', 'latepoint' )
			);
		} else {
			$weekdays = array(
				__( 'Mon', 'latepoint' ),
				__( 'Tue', 'latepoint' ),
				__( 'Wed', 'latepoint' ),
				__( 'Thu', 'latepoint' ),
				__( 'Fri', 'latepoint' ),
				__( 'Sat', 'latepoint' ),
				__( 'Sun', 'latepoint' )
			);
		}

		return $weekdays;
	}

	public static function get_weekday_name_by_number( $weekday_number, $full_name = false ) {
		$weekdays = OsBookingHelper::get_weekdays_arr( $full_name );
		if ( ! isset( $weekday_number ) || $weekday_number < 1 || $weekday_number > 7 ) {
			return '';
		} else {
			return $weekdays[ $weekday_number - 1 ];
		}
	}

	public static function get_stat( $stat, $args = [] ) {
		if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
			return false;
		}
		$defaults   = [
			'customer_id'    => false,
			'agent_id'       => false,
			'service_id'     => false,
			'location_id'    => false,
			'date_from'      => false,
			'date_to'        => false,
			'group_by'       => false,
			'exclude_status' => false
		];
		$args       = array_merge( $defaults, $args );
		$bookings   = new OsBookingModel();
		$query_args = array( $args['date_from'], $args['date_to'] );
		switch ( $stat ) {
			case 'duration':
				$stat_query = 'SUM(end_time - start_time)';
				break;
			case 'price':
				$stat_query = 'sum(total)';
				break;
			case 'bookings':
				$stat_query = 'count(id)';
				break;
		}
		$select_query = $stat_query . ' as stat';
		if ( $args['group_by'] ) {
			$select_query .= ',' . $args['group_by'];
		}
		$bookings->select( $select_query );


		if ( $args['date_from'] ) {
			$bookings->where( [ 'start_date >=' => $args['date_from'] ] );
		}
		if ( $args['date_to'] ) {
			$bookings->where( [ 'start_date <=' => $args['date_to'] ] );
		}
		if ( $args['service_id'] ) {
			$bookings->where( [ 'service_id' => $args['service_id'] ] );
		}
		if ( $args['agent_id'] ) {
			$bookings->where( [ 'agent_id' => $args['agent_id'] ] );
		}
		if ( $args['location_id'] ) {
			$bookings->where( [ 'location_id' => $args['location_id'] ] );
		}
		if ( $args['customer_id'] ) {
			$bookings->where( [ 'customer_id' => $args['customer_id'] ] );
		}
		if ( $args['group_by'] ) {
			$bookings->group_by( $args['group_by'] );
		}
		// TODO, need to support custom status exclusions
		if ( $args['exclude_status'] == LATEPOINT_BOOKING_STATUS_CANCELLED ) {
			$bookings->should_not_be_cancelled();
		}

		$stat_total = $bookings->get_results( ARRAY_A );
		if ( $args['group_by'] ) {
			return $stat_total;
		} else {
			return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
		}
	}

	public static function get_new_customer_stat_for_period( DateTime $date_from, DateTime $date_to, \LatePoint\Misc\Filter $filter ) {
		// TODO make sure filter is respected
		$customers = new OsCustomerModel();

		return $customers->filter_allowed_records()->where( [
			'created_at >=' => $date_from->format( 'Y-m-d' ),
			'created_at <=' => $date_to->format( 'Y-m-d' )
		] )->count();
	}

	public static function get_stat_for_period( $stat, $date_from, $date_to, \LatePoint\Misc\Filter $filter, $group_by = false ) {
		if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
			return false;
		}
		if ( ! in_array( $group_by, [ false, 'agent_id', 'service_id', 'location_id' ] ) ) {
			return false;
		}
		$bookings = new OsBookingModel();
		switch ( $stat ) {
			case 'duration':
				$stat_query = 'SUM(end_time - start_time)';
				break;
			case 'price':
				$stat_query = 'sum(' . LATEPOINT_TABLE_ORDER_ITEMS . '.subtotal)';
				$bookings->join( LATEPOINT_TABLE_ORDER_ITEMS, [ 'id' => $bookings->table_name . '.order_item_id' ] );
				$bookings->join( LATEPOINT_TABLE_ORDERS, [ 'id' => LATEPOINT_TABLE_ORDER_ITEMS . '.order_id' ] );
				break;
			case 'bookings':
				$stat_query = 'count(id)';
				break;
		}
		$select_query = $stat_query . ' as stat';
		if ( $group_by ) {
			$select_query .= ',' . $group_by;
		}
		$bookings->select( $select_query )->where( [ 'start_date >=' => $date_from, 'start_date <= ' => $date_to ] );

		if ( $filter->service_id ) {
			$bookings->where( [ 'service_id' => $filter->service_id ] );
		}
		if ( $filter->agent_id ) {
			$bookings->where( [ 'agent_id' => $filter->agent_id ] );
		}
		if ( $filter->location_id ) {
			$bookings->where( [ 'location_id' => $filter->location_id ] );
		}

		$bookings->should_not_be_cancelled();

		if ( $group_by ) {
			$bookings->group_by( $group_by );
		}

		$stat_total = $bookings->get_results( ARRAY_A );
		if ( $group_by ) {
			return $stat_total;
		} else {
			return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
		}
	}

	public static function get_total_bookings_per_day_for_period( $date_from, $date_to, \LatePoint\Misc\Filter $filter ) {
		$bookings = new OsBookingModel();
		$bookings->select( 'count(id) as bookings_per_day, start_date' )
		         ->where( [ 'start_date >=' => $date_from, 'start_date <=' => $date_to ] )
		         ->where( [ 'status NOT IN' => OsCalendarHelper::get_booking_statuses_hidden_from_calendar() ] );
		if ( $filter->service_id ) {
			$bookings->where( [ 'service_id' => $filter->service_id ] );
		}
		if ( $filter->agent_id ) {
			$bookings->where( [ 'agent_id' => $filter->agent_id ] );
		}
		if ( $filter->location_id ) {
			$bookings->where( [ 'location_id' => $filter->location_id ] );
		}
		$bookings->group_by( 'start_date' );

		return $bookings->get_results();
	}


	public static function get_min_max_work_periods( $specific_weekdays = false, $service_id = false, $agent_id = false ) {
		$select_string = 'MIN(start_time) as start_time, MAX(end_time) as end_time';
		$work_periods  = new OsWorkPeriodModel();
		$work_periods  = $work_periods->select( $select_string );
		$query_args    = array( 'service_id' => 0, 'agent_id' => 0 );
		if ( $service_id ) {
			$query_args['service_id'] = $service_id;
		}
		if ( $agent_id ) {
			$query_args['agent_id'] = $agent_id;
		}
		if ( $specific_weekdays && ! empty( $specific_weekdays ) ) {
			$query_args['week_day'] = $specific_weekdays;
		}
		$results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
		if ( ( $service_id || $agent_id ) && empty( $results['min_start_time'] ) ) {
			if ( $service_id && empty( $results['min_start_time'] ) ) {
				$query_args['service_id'] = 0;
				$work_periods             = new OsWorkPeriodModel();
				$work_periods             = $work_periods->select( $select_string );
				$results                  = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
			}
			if ( $agent_id && empty( $results['min_start_time'] ) ) {
				$query_args['agent_id'] = 0;
				$work_periods           = new OsWorkPeriodModel();
				$work_periods           = $work_periods->select( $select_string );
				$results                = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
			}
		}
		if ( $results ) {
			return array( $results['start_time'], $results['end_time'] );
		} else {
			return false;
		}
	}


	public static function get_work_start_end_time_for_multiple_dates( $dates = false, $service_id = false, $agent_id = false ) {
		$specific_weekdays = array();
		if ( $dates ) {
			foreach ( $dates as $date ) {
				$target_date = new OsWpDateTime( $date );
				$weekday     = $target_date->format( 'N' );
				if ( ! in_array( $weekday, $specific_weekdays ) ) {
					$specific_weekdays[] = $weekday;
				}
			}
		}
		$work_minmax_start_end = self::get_min_max_work_periods( $specific_weekdays, $service_id, $agent_id );

		return $work_minmax_start_end;
	}

	/**
	 * @param int $minute
	 * @param \LatePoint\Misc\WorkPeriod[] $work_periods_arr
	 *
	 * @return bool
	 */
	public static function is_minute_in_work_periods( int $minute, array $work_periods_arr ): bool {
		// print_r($work_periods_arr);
		if ( empty( $work_periods_arr ) ) {
			return false;
		}
		foreach ( $work_periods_arr as $work_period ) {
			// end of period does not count because we cant make appointment with 0 duration
			if ( $work_period->start_time <= $minute && $work_period->end_time > $minute ) {
				return true;
			}
		}

		return false;
	}

	public static function get_calendar_start_end_time( $bookings, $work_start_minutes, $work_end_minutes ) {
		$calendar_start_minutes = $work_start_minutes;
		$calendar_end_minutes   = $work_end_minutes;
		if ( $bookings ) {
			foreach ( $bookings as $bookings_for_agent ) {
				if ( $bookings_for_agent ) {
					foreach ( $bookings_for_agent as $booking ) {
						if ( $booking->start_time < $calendar_start_minutes ) {
							$calendar_start_minutes = $booking->start_time;
						}
						if ( $booking->end_time > $calendar_end_minutes ) {
							$calendar_end_minutes = $booking->end_time;
						}
					}
				}
			}
		}

		return [ $calendar_start_minutes, $calendar_end_minutes ];
	}

	public static function generate_direct_manage_booking_url( OsBookingModel $booking, string $for ): string {
		if ( ! in_array( $for, [ 'agent', 'customer' ] ) ) {
			return '';
		}
		$key = $booking->get_key_to_manage_for($for);
		$url = OsRouterHelper::build_admin_post_link( [ 'manage_booking_by_key', 'show' ], [ 'key' => $key ] );

		return $url;
	}

    public static function generate_summary_actions_for_booking(OsBookingModel $booking, ?string $key = null){
        ?>
        <div class="booking-full-summary-actions">
		  <div class="add-to-calendar-wrapper">
		    <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>
			  <?php echo OsBookingHelper::generate_add_to_calendar_links($booking, $key ?? $booking->get_key_to_manage_for('customer')); ?>
		  </div>
	    <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>
          <?php
			if($booking->is_upcoming()){
				if(OsCustomerHelper::can_reschedule_booking($booking)){ ?>
					<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">
						<i class="latepoint-icon latepoint-icon-calendar"></i>
						<span><?php esc_html_e('Reschedule', 'latepoint'); ?></span>
					</a>
					<?php
				}
				if(OsCustomerHelper::can_cancel_booking($booking)){ ?>
					<a href="#" class="booking-summary-action-btn cancel-appointment-btn"
					   data-os-prompt="<?php esc_attr_e('Are you sure you want to cancel this appointment?', 'latepoint'); ?>"
					   data-os-success-action="reload"
					   data-os-action="<?php echo esc_attr(OsRouterHelper::build_route_name('manage_booking_by_key', 'request_cancellation')); ?>"
					   data-os-params="<?php echo esc_attr(OsUtilHelper::build_os_params(['key' => $key ?? $booking->get_key_to_manage_for('customer')])); ?>">
						<i class="latepoint-icon latepoint-icon-ui-24"></i>
						<span><?php esc_html_e('Cancel', 'latepoint'); ?></span>
					</a>
					<?php
				}
			}
            do_action('latepoint_booking_summary_after_booking_actions', $booking);
            ?>
	  </div>
        <?php
    }

	public static function generate_summary_for_booking( OsBookingModel $booking, $cart_item_id = false, ?string $viewer = 'customer' ): string {
        $summary_html = '';
        $summary_html.= apply_filters( 'latepoint_booking_summary_before_summary_box', '', $booking );
		$summary_html.= '<div class="summary-box main-box" ' . ( ( $cart_item_id ) ? 'data-cart-item-id="' . $cart_item_id . '"' : '' ) . '>';
        $output_timezone_name = $viewer == 'customer' ? $booking->get_customer_timezone_name() : OsTimeHelper::get_wp_timezone_name();
        if(!empty($booking->start_datetime_utc)) {
	        $summary_html .= '<div class="summary-box-booking-date-box">';
	        $summary_html .= '<div class="summary-box-booking-date-day">' . $booking->start_datetime_in_format( 'j', $output_timezone_name ) . '</div>';
	        $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>';
            $summary_html .= '</div>';
        }
        $summary_html.= '<div class="summary-box-inner">';
		$service_headings = [];
		$service_headings = apply_filters( 'latepoint_booking_summary_service_headings', $service_headings, $booking );
		if ( $service_headings ) {
			$summary_html .= '<div class="summary-box-heading">';
			foreach ( $service_headings as $heading ) {
				$summary_html .= '<div class="sbh-item">' . $heading . '</div>';
			}
			$summary_html .= '<div class="sbh-line"></div>';
			$summary_html .= '</div>';
		}
		$summary_html .= '<div class="summary-box-content os-cart-item">';
		if ( $cart_item_id && OsCartsHelper::can_checkout_multiple_items() ) {
			$summary_html .= '<div class="os-remove-item-from-cart" role="button" tabindex="0" data-confirm-text="' . __( 'Are you sure you want to remove this item from your cart?', 'latepoint' ) . '" data-cart-item-id="' . $cart_item_id . '" data-route="' . OsRouterHelper::build_route_name( 'carts', 'remove_item_from_cart' ) . '">
															<div class="os-remove-from-cart-icon"></div>
														</div>';
		}
		$summary_html .= '<div class="sbc-big-item">' . $booking->get_service_name_for_summary() . '</div>';
		if ( $booking->start_date ) {
            $summary_html .= '<div class="sbc-highlighted-item">' . $booking->get_nice_datetime_for_summary($viewer) . '</div>';
		}
        /**
         * Output summary of the booking data after a start date and time
         *
         * @since 5.2.0
         * @hook latepoint_summary_booking_info_after_start_date
         *
         * @param {string} $summary_html HTML of the summary
         * @param {OsBookingModel} $booking Booking object that is being outputted
         * @param {string} $cart_item_id ID of a cart item this booking belongs to
         * @param {string} $viewer determines who is viewing this summary, can be customer or agent
         *
         * @returns {string} Filtered HTML
         */
        $summary_html = apply_filters('latepoint_summary_booking_info_after_start_date', $summary_html, $booking, $cart_item_id, $viewer);
		$summary_html .= '</div>';

		$service_attributes = [];
		$service_attributes = apply_filters( 'latepoint_booking_summary_service_attributes', $service_attributes, $booking );
		if ( $service_attributes ) {
			$summary_html .= '<div class="summary-attributes sa-clean">';
			foreach ( $service_attributes as $attribute ) {
				$summary_html .= '<span>' . $attribute['label'] . ': <strong>' . $attribute['value'] . '</strong></span>';
			}
			$summary_html .= '</div>';
		}
		$summary_html .= '</div>';
		$summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box_inner', '', $booking );
		$summary_html .= '</div>';
        $summary_html.= apply_filters( 'latepoint_booking_summary_after_summary_box', '', $booking );

		return $summary_html;
	}


	/**
	 * @param OsBookingModel[] $bookings
	 *
	 * @return bool
	 */
	public static function bookings_have_same_agent( array $bookings ): bool {
		return ( count( array_unique( array_column( $bookings, 'agent_id' ) ) ) == 1 );
	}

	/**
	 * @param OsBookingModel[] $bookings
	 *
	 * @return bool
	 */
	public static function bookings_have_same_location( array $bookings ): bool {
		return ( count( array_unique( array_column( $bookings, 'location_id' ) ) ) == 1 );
	}

	/**
	 * @param OsBookingModel[] $bookings
	 *
	 * @return bool
	 */
	public static function bookings_have_same_service( array $bookings ): bool {
		return ( count( array_unique( array_column( $bookings, 'service_id' ) ) ) == 1 );
	}

	public static function prepare_new_from_params( array $params ): OsBookingModel {
		$booking = new OsBookingModel();

		$services = OsServiceHelper::get_allowed_active_services();
		$agents   = OsAgentHelper::get_allowed_active_agents();

		// LOAD FROM PASSED PARAMS
		$booking->order_item_id = $params['order_item_id'] ?? '';
		$booking->service_id    = ! empty( $params['service_id'] ) ? OsUtilHelper::first_value_if_array( $params['service_id'] ) : '';
		if ( empty( $booking->service_id ) && ! empty( $services ) ) {
			$booking->service_id = $services[0]->id;
		}

		$booking->agent_id = ! empty( $params['agent_id'] ) ? OsUtilHelper::first_value_if_array( $params['agent_id'] ) : '';
		if ( empty( $booking->agent_id ) && ! empty( $agents ) ) {
			$booking->agent_id = $agents[0]->id;
		}

		if ( ! empty( $params['order_id'] ) ) {
			$order                = new OsOrderModel( $params['order_id'] );
			$booking->customer_id = $order->customer_id;
		} else {
			$booking->customer_id = ! empty( $params['customer_id'] ) ? OsUtilHelper::first_value_if_array( $params['customer_id'] ) : '';
		}

		$booking->location_id = ! empty( $params['location_id'] ) ? OsUtilHelper::first_value_if_array( $params['location_id'] ) : OsLocationHelper::get_default_location_id( true );
		$booking->start_date  = $params['start_date'] ?? OsTimeHelper::today_date( 'Y-m-d' );
		$booking->start_time  = $params['start_time'] ?? 600;

		$booking->end_time      = ( $booking->service_id ) ? $booking->calculate_end_time() : $booking->start_time + 60;
		$booking->end_date      = $booking->calculate_end_date();
		$booking->buffer_before = ( $booking->service_id ) ? $booking->service->buffer_before : 0;
		$booking->buffer_after  = ( $booking->service_id ) ? $booking->service->buffer_after : 0;
		$booking->status        = LATEPOINT_BOOKING_STATUS_APPROVED;

		return $booking;
	}


}