<?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; } }