plugin = $plugin; $this->hooks(); } /** * Initiate our hooks. * * @since 0.0.3 */ public function hooks() { add_filter( 'ssa/buffer_period/start_date', array( $this, 'get_period_with_buffered_start_date' ), 10, 3 ); add_filter( 'ssa/buffer_period/end_date', array( $this, 'get_period_with_buffered_end_date' ), 10, 3 ); } public function get_args( $appointment_type, $args = array() ) { $default_args = apply_filters( 'ssa/availability/default_args', array( 'start_date' => '', 'duration' => '', 'end_date' => '', 'start_date_min' => '', 'start_date_max' => '', 'end_date_min' => '', 'end_date_max' => '', 'buffered' => false, 'excluded_appointment_ids' => array(), 'appointment_statuses' => SSA_Appointment_Model::get_unavailable_statuses(), ), $appointment_type ); $args = shortcode_atts( $default_args, $args ); return $args; } public function get_period_with_buffered_start_date( $period, $original_period, $appointment_type ) { ssa_defensive_timezone_fix(); if ( !empty( $appointment_type['buffer_before'] ) ) { $buffer_before = '-' . absint( $appointment_type['buffer_before'] ) . ' MIN'; $period = $period->moveStartDate( $buffer_before ); } ssa_defensive_timezone_reset(); return $period; } public function get_period_with_buffered_end_date( $period, $original_period, $appointment_type ) { ssa_defensive_timezone_fix(); if ( !empty( $appointment_type['buffer_after'] ) ) { $buffer_after = '+' . absint( $appointment_type['buffer_after'] ) . ' MIN'; $period = $period->moveEndDate( $buffer_after ); } ssa_defensive_timezone_reset(); return $period; } public function is_period_available( $appointment_type, $args=array() ) { ssa_defensive_timezone_fix(); if ( (int)$appointment_type == $appointment_type ) { $appointment_type = $this->plugin->appointment_type_model->get( $appointment_type ); } if ( empty( $appointment_type['id'] ) ) { ssa_defensive_timezone_reset(); return new WP_Error(); } $args = $this->get_args( $appointment_type, $args ); if ( empty( $args['duration'] ) ) { $args['duration'] = $appointment_type['duration']; } if ( empty( $args['duration'] ) ) { ssa_defensive_timezone_reset(); return false; } if ( empty( $args['end_date'] ) ) { $args['end_date'] = ssa_datetime( $args['start_date'] )->add( new DateInterval( 'PT'.$appointment_type['duration'].'M' ) )->format( 'Y-m-d H:i:s' ); } if ( empty( $args['start_date_min'] ) ) { $args['start_date_min'] = ssa_datetime( $args['start_date'] )->sub( new DateInterval( 'PT'.$appointment_type['duration'].'M' ) )->sub( new DateInterval( 'P7D' ) )->format( 'Y-m-d H:i:s' ); } if ( empty( $args['start_date_max'] ) ) { $args['start_date_max'] = ssa_datetime( $args['start_date'] )->add( new DateInterval( 'PT'.$appointment_type['duration'].'M' ) )->add( new DateInterval( 'PT'.$appointment_type['duration'].'M' ) )->add( new DateInterval( 'P7D' ) )->format( 'Y-m-d H:i:s' ); } $desired_period = new Period( ssa_datetime( $args['start_date'] ), ssa_datetime( $args['end_date'] ) ); $bookable_appointments = $this->get_bookable_appointments( $appointment_type, $args ); foreach ( $bookable_appointments as $bookable_appointment_key => $bookable_appointment ) { if ( $bookable_appointment['period']->contains( $desired_period ) ) { ssa_defensive_timezone_reset(); return true; } } ssa_defensive_timezone_reset(); return false; } public function get_all_available_periods( $appointment_type, $args=array() ) { if ( (int)$appointment_type == $appointment_type ) { $appointment_type = $this->plugin->appointment_type_model->get( $appointment_type ); } if ( empty( $appointment_type['id'] ) ) { return new WP_Error(); } $args = $this->get_args( $appointment_type, $args ); ssa_defensive_timezone_fix(); if ( $this->plugin->settings_installed->is_enabled( 'advanced_scheduling' ) ) { if ( ! empty( $appointment_type['availability_start_date'] ) && $appointment_type['availability_start_date'] !== '0000-00-00 00:00:00' ) { if ( empty( $args['start_date_min'] ) || $args['start_date_min'] < $appointment_type['availability_start_date'] ) { $args['start_date_min'] = $appointment_type['availability_start_date']; } if ( $args['start_date_max'] < $args['start_date_min'] ) { $args['start_date_max'] = ssa_datetime( $appointment_type['availability_start_date'] )->add( new DateInterval( 'P30D' ) )->format( 'Y-m-d H:i:s' ); } } } if ( $this->plugin->settings_installed->is_enabled( 'advanced_scheduling' ) ) { if ( ! empty( $appointment_type['availability_end_date'] ) && $appointment_type['availability_end_date'] !== '0000-00-00 00:00:00' && ( empty( $args['start_date_max'] ) || $args['start_date_max'] > $appointment_type['availability_end_date'] ) ) { $args['start_date_max'] = $appointment_type['availability_end_date']; if ( $args['start_date_min'] > $args['start_date_max'] ) { ssa_defensive_timezone_reset(); return array(); } } } if ( empty( $appointment_type['availability_type'] ) || $appointment_type['availability_type'] != 'custom') { $available_periods = $this->get_default_available_periods( $appointment_type, $args ); } else { // TODO: support custom available periods // $available_periods = $this->get_custom_available_periods( $appointment_type['id'], $args ); } ssa_defensive_timezone_reset(); return $available_periods; } public function get_custom_available_periods( $appointment_type, $args=array() ) { ssa_defensive_timezone_fix(); if ( (int)$appointment_type == $appointment_type ) { $appointment_type = $this->plugin->appointment_type_model->get( $appointment_type ); } if ( empty( $appointment_type['id'] ) ) { ssa_defensive_timezone_reset(); return new WP_Error(); } $args = $this->get_args( $appointment_type, $args ); $custom_available_blocks = $this->plugin->availability_model->query( array_merge( array( 'appointment_type_id' => $appointment_type['id'], 'is_available' => 1, 'orderby' => 'start_date', 'order' => 'ASC', ), $args ) ); $custom_available_periods = array(); foreach ($custom_available_blocks as $key => $custom_available_block) { $custom_available_periods[] = new Period( $custom_available_block['start_date'], $custom_available_block['end_date'] ); } $custom_available_periods = $this->combine_abutting_periods( $custom_available_periods ); ssa_defensive_timezone_reset(); return $custom_available_periods; } public function get_default_available_periods( $appointment_type, $args ) { ssa_defensive_timezone_fix(); if ( (int)$appointment_type == $appointment_type ) { $appointment_type = $this->plugin->appointment_type_model->get( $appointment_type ); } if ( empty( $appointment_type['id'] ) ) { ssa_defensive_timezone_reset(); return new WP_Error(); } $args = $this->get_args( $appointment_type, $args ); $availability_home_timezone = $this->plugin->utils->get_datetimezone( $appointment_type['id'] ); $default_available_periods = array(); $start_date = ssa_datetime( $args['start_date_min'] ); $end_date = ssa_datetime( $args['start_date_max'] ); $start_date = $start_date->sub( new DateInterval( 'P1D' ) ); // help avoid timezone issues, our get_bookable_appointments() function will remove ones we don't need $end_date = $end_date->add( new DateInterval( 'P1D' ) ); // help avoid timezone issues, our get_bookable_appointments() function will remove ones we don't need $range_period = new Period( $start_date, $end_date ); foreach ($range_period->getDatePeriod( '1 DAY' ) as $datetime) { $day_of_week = $datetime->format( 'l' ); if ( empty( $appointment_type['availability'][$day_of_week][0]['time_start'] ) ) { continue; } foreach ($appointment_type['availability'][$day_of_week] as $time_block ) { $start_date = SSA_Utils::get_datetime_in_utc( $datetime->format( 'Y-m-d '.$time_block['time_start'] ), $availability_home_timezone ); if ( 'start_times' === $appointment_type['availability_type'] ) { $end_date = $start_date->add( new DateInterval( 'PT'.$appointment_type['duration'].'M' ) ); } else { // available_blocks (default behavior) $end_date = SSA_Utils::get_datetime_in_utc( $datetime->format( 'Y-m-d '.$time_block['time_end'] ), $availability_home_timezone ); } $new_period = new Period( $start_date, $end_date ); $default_available_periods[] = $new_period; } } $default_available_periods = $this->combine_abutting_periods( $default_available_periods ); ssa_defensive_timezone_reset(); return $default_available_periods; } public function get_booked_periods( $appointment_type, $args ) { ssa_defensive_timezone_fix(); $args = $this->get_args( $appointment_type, $args ); if ( (int)$appointment_type == $appointment_type ) { $appointment_type = $this->plugin->appointment_type_model->get( $appointment_type ); } if ( empty( $appointment_type['id'] ) ) { ssa_defensive_timezone_reset(); return new WP_Error(); } if ( apply_filters( 'ssa/get_booked_periods/should_separate_availability_for_appointment_types', false ) ) { $args['appointment_type_id'] = $appointment_type['id']; } if ( empty( $args['appointment_type_id'] ) ) { $developer_settings = $this->plugin->developer_settings->get(); if ( ! empty( $developer_settings['separate_appointment_type_availability'] ) ) { $args['appointment_type_id'] = $appointment_type['id']; } } $booked_blocks = $this->plugin->appointment_model->query( array_merge( array( 'status' => $args['appointment_statuses'], 'number' => -1, ), $args ) ); $booked_periods = array(); foreach ($booked_blocks as $key => $booked_block) { if ( ! empty( $args['excluded_appointment_ids'] ) && is_array( $args['excluded_appointment_ids'] ) ) { if ( in_array( $booked_block['id'], $args['excluded_appointment_ids'] ) ) { continue; } } if ( $booked_block['end_date'] <= $booked_block['start_date'] ) { $booked_block['end_date'] = ssa_datetime( $booked_block['start_date'] )->add( new DateInterval( 'PT'.$appointment_type['duration'].'M' ) )->format( 'Y-m-d H:i:s' ); } if ( $booked_block['end_date'] < $booked_block['start_date'] ) { ssa_debug_log( __CLASS__ . ' ' . __FUNCTION__ . '():' . __LINE__ ); ssa_debug_log( 'The ending datepoint must be greater or equal to the starting datepoint in appointment ID ' . $this->id, 10 ); $booked_period = new Period( $booked_block['start_date'], $booked_block['start_date'] ); } else { $booked_period = new Period( $booked_block['start_date'], $booked_block['end_date'] ); } if ( !empty( $args['buffered'] ) ) { $booked_period = apply_filters( 'ssa/buffer_period/start_date', $booked_period, $booked_period, $appointment_type ); $booked_period = apply_filters( 'ssa/buffer_period/end_date', $booked_period, $booked_period, $appointment_type ); if ( empty( $booked_period ) ) { continue; } } $booked_periods[] = $booked_period; } $booked_periods = apply_filters( 'ssa/get_booked_periods/booked_periods', $booked_periods, $appointment_type ); ssa_defensive_timezone_reset(); return $booked_periods; } public function get_blocked_periods( $appointment_type, $args ) { if ( (int)$appointment_type == $appointment_type ) { $appointment_type = $this->plugin->appointment_type_model->get( $appointment_type ); } if ( empty( $appointment_type['id'] ) ) { return new WP_Error(); } $args = $this->get_args( $appointment_type, $args ); $blocked_periods = array(); ssa_defensive_timezone_fix(); $blocked_periods = apply_filters( 'ssa/get_blocked_periods/blocked_periods', $blocked_periods, $appointment_type, $args ); ssa_defensive_timezone_reset(); return $blocked_periods; } public function get_bookable_appointments( $appointment_type, $args=array() ) { ssa_defensive_timezone_fix(); if ( (int)$appointment_type == $appointment_type ) { $appointment_type = $this->plugin->appointment_type_model->get( $appointment_type ); } if ( empty( $appointment_type['id'] ) ) { ssa_defensive_timezone_reset(); return new WP_Error(); } $appointment_type = apply_filters( 'ssa/get_bookable_appointments/appointment_type', $appointment_type ); $availability_home_timezone = $this->plugin->utils->get_datetimezone( $appointment_type['id'] ); $args = $this->get_args( $appointment_type, $args ); if ( !empty( $args['start_date_min'] ) ) { $start_date_min_datetime = ssa_datetime( $args['start_date_min'] ); $start_date_with_min_booking_notice_datetime = ssa_datetime(); if ( !empty( $appointment_type['min_booking_notice'] ) ) { $start_date_with_min_booking_notice_datetime = ssa_datetime()->add( new DateInterval( 'PT'.$appointment_type['min_booking_notice'].'M' ) ); } if ( $start_date_with_min_booking_notice_datetime > $start_date_min_datetime ) { $start_date_min_datetime = $start_date_with_min_booking_notice_datetime; } } if ( !empty( $args['start_date_max'] ) ) { $start_date_max_datetime = ssa_datetime( $args['start_date_max'] ); } if ( $this->plugin->settings_installed->is_enabled( 'advanced_scheduling' ) ) { if ( !empty( $appointment_type['max_booking_notice'] ) ) { $start_date_with_max_booking_notice_datetime = ssa_datetime()->add( new DateInterval( 'PT'.$appointment_type['max_booking_notice'].'M' ) ); if ( empty( $start_date_max_datetime ) || $start_date_with_max_booking_notice_datetime < $start_date_max_datetime ) { $start_date_max_datetime = $start_date_with_max_booking_notice_datetime; } } } if ( empty( $appointment_type['availability_increment'] ) ) { $appointment_type['availability_increment'] = 15; } if ( !empty( $args['end_date_min'] ) ) { $end_date_min_datetime = ssa_datetime( $args['end_date_min'] ); } if ( !empty( $args['end_date_max'] ) ) { $end_date_max_datetime = ssa_datetime( $args['end_date_max'] ); } if ( $this->plugin->settings_installed->is_enabled( 'advanced_scheduling' ) ) { if ( !empty( $appointment_type['max_booking_notice'] ) ) { $end_date_with_max_booking_notice_datetime = ssa_datetime()->add( new DateInterval( 'PT'.$appointment_type['max_booking_notice'].'M' ) )->add( new DateInterval( 'PT'.$appointment_type['duration'].'M' ) ); if ( empty( $end_date_max_datetime ) || $end_date_with_max_booking_notice_datetime < $end_date_max_datetime ) { $end_date_max_datetime = $end_date_with_max_booking_notice_datetime; } } } $available_periods = $this->get_all_available_periods( $appointment_type, $args ); $booked_periods = $this->get_booked_periods( $appointment_type, $args ); $blocked_periods = $this->get_blocked_periods( $appointment_type, $args ); $bookable_periods = array(); foreach ($available_periods as $key => $available_period) { foreach ($available_period->getDatePeriod($appointment_type['availability_increment'].' MIN') as $start_datetime) { /* Look at every bookable start time and determine if this slot is available */ /* Does this potential appointment start within min/max datetime? */ if ( !empty( $start_date_min_datetime ) && $start_datetime < $start_date_min_datetime ) { continue; } if ( !empty( $start_date_max_datetime ) && $start_datetime > $start_date_max_datetime ) { continue; } /* If this is a "start_times" availability type, is it a valid start time? */ if ( 'start_times' === $appointment_type['availability_type'] ) { $local_start_datetime = $start_datetime->setTimezone( $availability_home_timezone ); $day_of_week = $local_start_datetime->format( 'l' ); if ( empty( $appointment_type['availability'][$day_of_week][0]['time_start'] ) ) { continue; // no start times for this day of the week } $is_valid_start_time = false; foreach ($appointment_type['availability'][$day_of_week] as $key => $slot) { if ( $is_valid_start_time ) { continue; } if ( $slot['time_start'] == $local_start_datetime->format( 'H:i:s' ) ) { $is_valid_start_time = true; } if ( $slot['time_start'] == $local_start_datetime->format( 'G:i:s' ) ) { $is_valid_start_time = true; } } if ( ! $is_valid_start_time ) { continue; } } $bookable_period = Period::after($start_datetime, new DateInterval('PT'.$appointment_type['duration'].'M')); /* Does this potential appointment end within min/max datetime? */ if ( !empty( $end_date_min_datetime ) && $bookable_period->getEndDate() < $end_date_min_datetime ) { continue; } if ( !empty( $end_date_max_datetime ) && $bookable_period->getEndDate() > $end_date_max_datetime ) { continue; } /* Does this potential appointment fit within the available block? */ if ( !$available_period->contains( $bookable_period ) ) { continue; } /* Does this potential appointment conflict with a blocked time? */ $buffered_bookable_period = apply_filters( 'ssa/buffer_period/start_date', $bookable_period, $bookable_period, $appointment_type ); $buffered_bookable_period = apply_filters( 'ssa/buffer_period/end_date', $buffered_bookable_period, $bookable_period, $appointment_type ); foreach ($blocked_periods as $key => $blocked_period) { $buffered_blocked_period = apply_filters( 'ssa/buffer_period/blocked_period', $blocked_period, $blocked_period, $appointment_type ); if ( $buffered_bookable_period->overlaps( $buffered_blocked_period ) ) { /* They overlap, but let's make sure that our double-buffering didn't exclude a valid bookable period (collapse margins) */ if ( $bookable_period->overlaps( $blocked_period ) ) { // even unbuffered they overlap continue 2; } $gap = $bookable_period->gap( $blocked_period ); $minimum_gap_in_seconds = 60 * max( absint( $appointment_type['buffer_before'] ), absint( $appointment_type['buffer_after'] ) ); if ( $gap->getTimestampInterval() < $minimum_gap_in_seconds ) { continue 2; } } } /* Does this potential appointment conflict with a previously-booked time? */ foreach ($booked_periods as $key => $booked_period) { $buffered_booked_period = apply_filters( 'ssa/buffer_period/start_date', $booked_period, $booked_period, $appointment_type ); $buffered_booked_period = apply_filters( 'ssa/buffer_period/end_date', $buffered_booked_period, $booked_period, $appointment_type ); if ( $buffered_bookable_period->overlaps( $buffered_booked_period ) ) { /* They overlap, but let's make sure that our double-buffering didn't exclude a valid bookable period (collapse margins) */ if ( $bookable_period->overlaps( $booked_period ) ) { // even unbuffered they overlap continue 2; } $gap = $bookable_period->gap( $booked_period ); $minimum_gap_in_seconds = 60 * max( absint( $appointment_type['buffer_before'] ), absint( $appointment_type['buffer_after'] ) ); if ( $gap->getTimestampInterval() < $minimum_gap_in_seconds ) { continue 2; } } } $bookable_periods[] = $bookable_period; } } $bookable_appointments = array(); foreach ($bookable_periods as $key => $bookable_period) { $bookable_appointments[] = array( 'period' => $bookable_period, ); } ssa_defensive_timezone_reset(); return $bookable_appointments; } /** Utility Functions **/ public function get_inverse_periods( $periods_array ) { $inverse_array = array(); $last_period = null; foreach ($periods_array as $key => $period) { if ( ! empty( $last_period ) ) { $gap_period = $last_period->gap( $period ); $inverse_array[] = $gap_period; } $last_period = $period; } return $inverse_array; } public function combine_abutting_periods( $periods_array ) { ssa_defensive_timezone_fix(); if ( !is_array( $periods_array ) ) { return $periods_array; } $combined_periods = array(); while( count( $periods_array ) ) { $next_period = array_shift( $periods_array ); if ( count( $combined_periods ) === 0 ) { $combined_periods[] = $next_period; continue; } $last_period = current(array_slice($combined_periods, -1)); if ( $last_period->abuts( $next_period ) ) { if ( $last_period->isAfter( $next_period ) ) { $tmp_last_period = $last_period; $last_period = $next_period; $next_period = $tmp_last_period; } array_pop( $combined_periods ); $combined_periods[] = new Period( $last_period->getStartDate(), $next_period->getEndDate() ); continue; } $combined_periods[] = $next_period; continue; } $combined_periods = array_filter($combined_periods, function($value) { ssa_defensive_timezone_reset(); return $value !== ''; }); ssa_defensive_timezone_reset(); return $combined_periods; } }