HEX
Server: LiteSpeed
System: Linux s787.bom1.mysecurecloudhost.com 4.18.0-477.13.1.lve.el8.x86_64 #1 SMP Thu Jun 1 16:40:47 EDT 2023 x86_64
User: mobilech (5348)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/mobilech/alliancelaptoptraining.com.np/wp-content/plugins/tutor/classes/Instructor.php
<?php
/**
 * Manage Instructor
 *
 * @package Tutor
 * @author Themeum <support@themeum.com>
 * @link https://themeum.com
 * @since 1.0.0
 */

namespace TUTOR;

defined( 'ABSPATH' ) || exit;

use DateInterval;
use DateTime;
use Tutor\GDPR\Controllers\LegalConsent;
use Tutor\Helpers\DateTimeHelper;
use Tutor\Helpers\QueryHelper;
use Tutor\Traits\JsonResponse;

/**
 * Instructor class
 *
 * @since 1.0.0
 */
class Instructor {
	use JsonResponse;

	/**
	 * Error message
	 *
	 * @var string
	 */
	protected $error_msgs = '';

	/**
	 * Constructor
	 *
	 * @since 1.0.0
	 *
	 * @param bool $register_hook register hook or not.
	 *
	 * @return void
	 */
	public function __construct( $register_hook = true ) {
		if ( ! $register_hook ) {
			return;
		}
		add_action( 'template_redirect', array( $this, 'register_instructor' ) );
		add_action( 'template_redirect', array( $this, 'apply_instructor' ) );

		// Add instructor from admin panel.
		add_action( 'wp_ajax_tutor_add_instructor', array( $this, 'add_new_instructor' ) );

		/**
		 * Instructor Approval
		 * Block Unblock
		 *
		 * @since 1.5.3
		 */
		add_action( 'wp_ajax_instructor_approval_action', array( $this, 'instructor_approval_action' ) );

		/**
		 * Check if instructor can publish courses
		 *
		 * @since 1.5.9
		 */
		add_action( 'tutor_option_save_after', array( $this, 'can_publish_tutor_courses' ) );

		/**
		 * Hide instructor rejection message
		 *
		 * @since 1.9.2
		 */
		add_action( 'wp_loaded', array( $this, 'hide_instructor_notice' ) );

		add_action( 'wp_ajax_tutor_save_instructor_home_sections_order', array( $this, 'ajax_save_home_sections_order' ) );
		add_action( 'wp_ajax_tutor_save_instructor_home_sections_visibility', array( $this, 'ajax_save_home_section_visibility' ) );
	}

	/**
	 * Template Redirect Callback
	 * For Register new user and mark him as instructor
	 *
	 * @since 1.0.0
	 * @return void|null
	 */
	public function register_instructor() {
		// Here tutor_action checking required before nonce checking.
		if ( 'tutor_register_instructor' !== Input::post( 'tutor_action' ) || ! get_option( 'users_can_register', false ) ) {
			return;
		}

		// Checking nonce.
		tutor_utils()->checking_nonce();
		$required_fields = apply_filters(
			'tutor_instructor_registration_required_fields',
			array(
				'first_name'            => __( 'First name field is required', 'tutor' ),
				'last_name'             => __( 'Last name field is required', 'tutor' ),
				'email'                 => __( 'E-Mail field is required', 'tutor' ),
				'user_login'            => __( 'User Name field is required', 'tutor' ),
				'password'              => __( 'Password field is required', 'tutor' ),
				'password_confirmation' => __( 'Password Confirmation field is required', 'tutor' ),
			)
		);

		$validation_errors = array();

		/*
		* Push into validation_errors
		* Error registration_errors
		*/
		$errors = apply_filters( 'registration_errors', new \WP_Error(), '', '' );
		foreach ( $errors->errors as $key => $value ) {
			$validation_errors[ $key ] = $value[0];
		}

		foreach ( $required_fields as $required_key => $required_value ) {
			if ( empty( $_POST[ $required_key ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
				$validation_errors[ $required_key ] = $required_value;
			}
		}

		$validate_consent = LegalConsent::validate_consent( LegalConsent::DISPLAY_ON_INS_REG, $_POST );
		if ( is_wp_error( $validate_consent ) ) {
			$validation_errors[ $validate_consent->get_error_code() ] = $validate_consent->get_error_message();
		}

		if ( ! filter_var( tutor_utils()->input_old( 'email' ), FILTER_VALIDATE_EMAIL ) ) {
			$validation_errors['email'] = __( 'Valid E-Mail is required', 'tutor' );
		}

		if ( tutor_utils()->input_old( 'password' ) !== tutor_utils()->input_old( 'password_confirmation' ) ) {
			$validation_errors['password_confirmation'] = __( 'Your passwords should match each other. Please recheck.', 'tutor' );
		}

		if ( count( $validation_errors ) ) {
			$this->error_msgs = $validation_errors;
			add_filter( 'tutor_instructor_register_validation_errors', array( $this, 'tutor_instructor_form_validation_errors' ) );
			return;
		}

		$first_name = sanitize_text_field( tutor_utils()->input_old( 'first_name' ) );
		$last_name  = sanitize_text_field( tutor_utils()->input_old( 'last_name' ) );
		$email      = sanitize_text_field( tutor_utils()->input_old( 'email' ) );
		$user_login = sanitize_text_field( tutor_utils()->input_old( 'user_login' ) );
		$password   = sanitize_text_field( tutor_utils()->input_old( 'password' ) );

		$userdata = array(
			'user_login' => $user_login,
			'user_email' => $email,
			'first_name' => $first_name,
			'last_name'  => $last_name,
			'user_pass'  => $password,
		);

		global $wpdb;
		$wpdb->query( 'START TRANSACTION' );
		$user_id = wp_insert_user( $userdata );
		if ( is_wp_error( $user_id ) ) {
			$this->error_msgs = $user_id->get_error_messages();
			add_filter( 'tutor_instructor_register_validation_errors', array( $this, 'tutor_instructor_form_validation_errors' ) );
			return;
		}

		$user = get_user_by( 'id', $user_id );

		$is_req_email_verification = apply_filters( 'tutor_require_email_verification', false );
		if ( $is_req_email_verification ) {
			do_action( 'tutor_send_verification_mail', get_userdata( $user_id ), 'instructor-registration' );
			$reg_done = apply_filters( 'tutor_registration_done', true );
			if ( ! $reg_done ) {
				$wpdb->query( 'ROLLBACK' );
				return;
			} else {
				$wpdb->query( 'COMMIT' );
			}
		} else {
			/**
			 * Tutor Free - regular instructor reg process.
			 */
			$this->update_instructor_meta( $user_id );
			$wpdb->query( 'COMMIT' );

			if ( $user ) {
				wp_set_current_user( $user_id, $user->user_login );
				wp_set_auth_cookie( $user_id );
				do_action( 'tutor_after_instructor_signup', $user_id );
			}
		}

		if ( $user ) {
			do_action( 'tutor_new_instructor_registered', $user_id, $validate_consent );
		}

		wp_safe_redirect( tutor_utils()->get_nocache_url( tutor_utils()->input_old( '_wp_http_referer' ) ) );
		die();
	}

	/**
	 * Get instructor reg validation errors.
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public function tutor_instructor_form_validation_errors() {
		return $this->error_msgs;
	}

	/**
	 * Template Redirect Callback
	 * for instructor applying when a user already logged in
	 *
	 * @since 1.0.0
	 * @return void|null
	 */
	public function apply_instructor() {
		// Here tutor_action checking required before nonce checking.
		if ( 'tutor_apply_instructor' !== Input::post( 'tutor_action' ) ) {
			return;
		}

		// Checking nonce.
		tutor_utils()->checking_nonce();

		$user_id = get_current_user_id();
		if ( $user_id ) {
			if ( tutor_utils()->is_instructor() ) {
				die( esc_html__( 'Already applied for instructor', 'tutor' ) );
			} else {
				update_user_meta( $user_id, '_is_tutor_instructor', tutor_time() );
				update_user_meta( $user_id, '_tutor_instructor_status', apply_filters( 'tutor_initial_instructor_status', 'pending' ) );
				update_user_meta( $user_id, User::APPLICATION_SOURCE_META, User::SOURCE_STUDENT_DASHBOARD );

				do_action( 'tutor_new_instructor_after', $user_id );
			}
		} else {
			die( esc_html__( 'Permission denied', 'tutor' ) );
		}

		wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
		die();
	}


	/**
	 * Add new instructor
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function add_new_instructor() {
		tutor_utils()->checking_nonce();

		// Only admin should be able to add instructor.
		if ( ! current_user_can( 'manage_options' ) || ! get_option( 'users_can_register', false ) ) {
			wp_send_json_error();
		}

		$required_fields = apply_filters(
			'tutor_instructor_registration_required_fields',
			array(
				'first_name'            => __( 'First name field is required', 'tutor' ),
				'last_name'             => __( 'Last name field is required', 'tutor' ),
				'email'                 => __( 'E-Mail field is required', 'tutor' ),
				'user_login'            => __( 'User Name field is required', 'tutor' ),
				'password'              => __( 'Password field is required', 'tutor' ),
				'password_confirmation' => __( 'Your passwords should match each other. Please recheck.', 'tutor' ),
			)
		);

		$validation_errors = array();
		foreach ( $required_fields as $required_key => $required_value ) {
			if ( empty( $_POST[ $required_key ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
				$validation_errors[ $required_key ] = $required_value;
			}
		}

		if ( ! filter_var( tutor_utils()->input_old( 'email' ), FILTER_VALIDATE_EMAIL ) ) {
			$validation_errors['email'] = __( 'Valid E-Mail is required', 'tutor' );
		}
		if ( tutor_utils()->input_old( 'password' ) !== tutor_utils()->input_old( 'password_confirmation' ) ) {
			$validation_errors['password_confirmation'] = __( 'Your passwords should match each other. Please recheck.', 'tutor' );
		}

		if ( count( $validation_errors ) ) {
			wp_send_json_error( array( 'errors' => $validation_errors ) );
		}

		$first_name              = sanitize_text_field( tutor_utils()->input_old( 'first_name' ) );
		$last_name               = sanitize_text_field( tutor_utils()->input_old( 'last_name' ) );
		$email                   = sanitize_text_field( tutor_utils()->input_old( 'email' ) );
		$user_login              = sanitize_text_field( tutor_utils()->input_old( 'user_login' ) );
		$phone_number            = sanitize_text_field( tutor_utils()->input_old( 'phone_number' ) );
		$password                = sanitize_text_field( tutor_utils()->input_old( 'password' ) );
		$tutor_profile_bio       = Input::post( 'tutor_profile_bio', '', Input::TYPE_KSES_POST );
		$tutor_profile_job_title = sanitize_text_field( tutor_utils()->input_old( 'tutor_profile_job_title' ) );

		$userdata = apply_filters(
			'add_new_instructor_data',
			array(
				'user_login' => $user_login,
				'user_email' => $email,
				'first_name' => $first_name,
				'last_name'  => $last_name,
				'role'       => tutor()->instructor_role,
				'user_pass'  => $password,
			)
		);

		do_action( 'tutor_add_new_instructor_before' );

		$user_id = wp_insert_user( $userdata );
		if ( ! is_wp_error( $user_id ) ) {
			update_user_meta( $user_id, 'phone_number', $phone_number );
			update_user_meta( $user_id, 'description', $tutor_profile_bio );
			update_user_meta( $user_id, '_tutor_profile_bio', $tutor_profile_bio );
			update_user_meta( $user_id, '_tutor_profile_job_title', $tutor_profile_job_title );
			update_user_meta( $user_id, '_is_tutor_instructor', tutor_time() );
			update_user_meta( $user_id, '_tutor_instructor_status', apply_filters( 'tutor_initial_instructor_status', 'approved' ) );

			do_action( 'tutor_add_new_instructor_after', $user_id );

			wp_send_json_success( array( 'msg' => __( 'Instructor has been added successfully', 'tutor' ) ) );
		}

		wp_send_json_error( array( 'errors' => $user_id ) );
	}

	/**
	 * Handle instructor approval action
	 * This function not used maybe, will be removed
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function instructor_approval_action() {
		tutor_utils()->checking_nonce();

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
		}

		$instructor_id = Input::post( 'instructor_id', 0, Input::TYPE_INT );
		$action        = Input::post( 'action_name' );

		if ( 'approve' === $action ) {
			do_action( 'tutor_before_approved_instructor', $instructor_id );

			update_user_meta( $instructor_id, '_tutor_instructor_status', 'approved' );
			update_user_meta( $instructor_id, '_tutor_instructor_approved', tutor_time() );
			update_user_meta( $instructor_id, User::INSTRUCTOR_APPROVAL_NOTICE_META, true );

			$instructor = new \WP_User( $instructor_id );
			$instructor->add_role( tutor()->instructor_role );

			// Send E-Mail to this user about instructor approval via hook.
			do_action( 'tutor_after_approved_instructor', $instructor_id );
		}

		if ( 'blocked' === $action ) {
			do_action( 'tutor_before_blocked_instructor', $instructor_id );
			update_user_meta( $instructor_id, '_tutor_instructor_status', 'blocked' );

			$instructor = new \WP_User( $instructor_id );
			$instructor->remove_role( tutor()->instructor_role );
			do_action( 'tutor_after_blocked_instructor', $instructor_id );

			// TODO: send E-Mail to this user about instructor blocked, should via hook.
		}

		if ( 'remove-instructor' === $action ) {
			do_action( 'tutor_before_rejected_instructor', $instructor_id );

			$user = new \WP_User( $instructor_id );
			$user->remove_role( tutor()->instructor_role );

			tutor_utils()->remove_instructor_role( $instructor_id );
			update_user_meta( $instructor_id, '_is_tutor_instructor_rejected', tutor_time() );
			update_user_meta( $instructor_id, 'tutor_instructor_show_rejection_message', true );

			// Send E-Mail to this user about instructor rejection via hook.
			do_action( 'tutor_after_rejected_instructor', $instructor_id );
		}

		wp_send_json_success();
	}

	/**
	 * Hide instructor notice
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function hide_instructor_notice() {
		if ( 'hide_instructor_notice' === Input::get( 'tutor_action' ) ) {
			delete_user_meta( get_current_user_id(), 'tutor_instructor_show_rejection_message' );
		} elseif ( 'hide_instructor_approval_notice' === Input::get( 'tutor_action' ) ) {
			delete_user_meta( get_current_user_id(), User::INSTRUCTOR_APPROVAL_NOTICE_META );
		}
	}

	/**
	 * Can instructor publish courses directly
	 * Fixed in Gutenberg
	 *
	 * @since 1.5.9
	 * @return void
	 */
	public function can_publish_tutor_courses() {
		$can_publish_course = (bool) tutor_utils()->get_option( 'instructor_can_publish_course' );

		$instructor_role = tutor()->instructor_role;
		$instructor      = get_role( $instructor_role );

		if ( $can_publish_course ) {
			$instructor->add_cap( 'publish_tutor_courses' );
		} else {
			$instructor->remove_cap( 'publish_tutor_courses' );
		}
	}

	/**
	 * Update instructor meta just after register
	 *
	 * @since 2.1.9
	 *
	 * @param integer $user_id user id.
	 *
	 * @return void
	 */
	public function update_instructor_meta( int $user_id ) {
		update_user_meta( $user_id, '_is_tutor_instructor', tutor_time() );
		update_user_meta( $user_id, '_tutor_instructor_status', apply_filters( 'tutor_initial_instructor_status', 'pending' ) );
		update_user_meta( $user_id, User::APPLICATION_SOURCE_META, User::SOURCE_INSTRUCTOR_REGISTRATION );

		do_action( 'tutor_new_instructor_after', $user_id );
	}

	/**
	 * Calculate the previous comparison date range based on a selected date range.
	 *
	 * @since 4.0.0
	 *
	 * @param string|null $selected_start_date Selected start date (Y-m-d).
	 * @param string|null $selected_end_date   Selected end date (Y-m-d).
	 *
	 * @return array {
	 *     @type string $previous_start_date Previous period start date (Y-m-d).
	 *     @type string $previous_end_date   Previous period end date (Y-m-d).
	 * }
	 */
	public static function get_comparison_date_range( $selected_start_date, $selected_end_date ) {

		$format = DateTimeHelper::FORMAT_DATE;

		if ( empty( $selected_start_date ) && empty( $selected_end_date ) ) {

			$now = DateTimeHelper::now();
			return array(
				'previous_start_date' => $now->create( 'first day of this month' )->format( $format ),
				'previous_end_date'   => $now->create( 'last day of this month' )->format( $format ),
			);
		}

		$start = new DateTime( $selected_start_date );
		$end   = new DateTime( $selected_end_date );
		$days  = $start->diff( $end )->days + 1;

		$previous_start_date = $start->sub( DateInterval::createFromDateString( "$days days" ) )->format( $format );
		$previous_end_date   = $end->sub( DateInterval::createFromDateString( "$days days" ) )->format( $format );

		return array(
			'previous_start_date' => $previous_start_date,
			'previous_end_date'   => $previous_end_date,
		);
	}

	/**
	 * Get course completion distribution data for a specific instructor.
	 *
	 * @since 4.0.0
	 *
	 * @param array $instructor_course_ids    Optional list of course IDs.
	 *
	 * @return array {
	 *     Enrollment distribution counts.
	 *
	 *     @type int $enrolled   Total number of enrollments.
	 *     @type int $completed  Number of fully completed enrollments (100% progress).
	 *     @type int $inprogress Number of enrollments with partial progress (>0 and <100).
	 *     @type int $inactive   Number of enrollments with no progress (0%).
	 *     @type int $cancelled  Number of cancelled enrollments.
	 * }
	 */
	public static function get_course_completion_distribution_data_by_instructor( $instructor_course_ids = array() ) {

		global $wpdb;

		$counts = array(
			'enrolled'   => 0,
			'completed'  => 0,
			'inprogress' => 0,
			'inactive'   => 0,
			'cancelled'  => 0,
		);

		$cancel_statuses = array( 'cancel', 'canceled', 'cancelled' );
		$post_statuses   = array_merge( $cancel_statuses, array( 'completed' ) );

		if ( empty( $instructor_course_ids ) ) {
			return $counts;
		}

		$where = array(
			'post_type'   => 'tutor_enrolled',
			'post_status' => array( 'IN', $post_statuses ),
			'post_parent' => array( 'IN', $instructor_course_ids ),
		);

		$args = array(
			'select' => array( 'id', 'post_status', 'post_author', 'post_parent' ),
			'where'  => $where,
		);

		$enrollments = QueryHelper::query( $wpdb->posts, $args );

		if ( empty( $enrollments ) ) {
			return $counts;
		}

		$counts['enrolled'] = count( $enrollments );

		foreach ( $enrollments as $enrollment ) {

			// Cancelled enrollment.
			if ( in_array( $enrollment->post_status, $cancel_statuses, true ) ) {
				++$counts['cancelled'];
				continue;
			}

			// Completed course.
			if ( tutor_utils()->is_completed_course( $enrollment->post_parent, $enrollment->post_author ) ) {
				++$counts['completed'];
				continue;
			}

			$course_progress = (int) tutor_utils()->get_course_completed_percent( $enrollment->post_parent, $enrollment->post_author );

			if ( 100 === $course_progress ) { // If progress is 100% but the `Complete Course` button hasn't been clicked.
				++$counts['completed'];
				continue;
			}

			if ( $course_progress > 0 ) {
				++$counts['inprogress'];
				continue;
			}

			++$counts['inactive'];
		}

		return $counts;
	}

	/**
	 * Retrieve the top-performing courses for a given instructor.
	 *
	 * @since 4.0.0
	 *
	 * @param int   $instructor_id Instructor  user ID.
	 * @param array $args {
	 *     Optional query arguments.
	 *
	 *     @type string $start_date Optional start date (Y-m-d).
	 *     @type string $end_date   Optional end date (Y-m-d).
	 *     @type string $order_by   Sorting criteria. Accepts 'revenue' or 'student'.
	 * }

	 * @param int   $limit         Maximum number of courses to return. Default 4.
	 *
	 * @return array List of course objects containing:
	 *               - course_id (int)
	 *               - course_title (string)
	 *               - total_revenue (float)
	 *               - total_student (int)
	 *
	 * @throws \Exception When a database error occurs.
	 */
	public static function get_top_performing_courses_by_instructor( $instructor_id, $args, $limit = 4 ) {

		global $wpdb;

		$start_date = $args['start_date'] ?? null;
		$end_date   = $args['end_date'] ?? null;
		$order_by   = 'revenue' === $args['order_by'] ? 'total_revenue' : 'total_student';

		$complete_status = tutor_utils()->get_earnings_completed_statuses();

		$amount_type = User::is_admin() && is_admin() ? 'earnings.admin_amount' : 'earnings.instructor_amount';
		$amount_rate = User::is_admin() && is_admin() ? 'earnings.admin_rate' : 'earnings.instructor_rate';

		$amount_condition = "CASE
			WHEN orders.tax_type = 'inclusive' AND earnings.course_price_grand_total > 0
				THEN ( earnings.course_price_grand_total - orders.tax_amount ) * ( $amount_rate/100 )
			ELSE $amount_type
			END";

		$earning_where_clause = array(
			'earnings.user_id'      => $instructor_id,
			'earnings.order_status' => array( 'IN', $complete_status ),
		);

		$enrollment_where_clause = array(
			'post_type'   => 'tutor_enrolled',
			'post_status' => 'completed',
		);

		if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
			$earning_where_clause['earnings.created_at'] = array( 'BETWEEN', array( $start_date, $end_date ) );
			$enrollment_where_clause['post_date']        = array( 'BETWEEN', array( $start_date, $end_date ) );
		}

		$earning_where_clause    = QueryHelper::prepare_where_clause( $earning_where_clause );
		$enrollment_where_clause = QueryHelper::prepare_where_clause( $enrollment_where_clause );

		$earnings_sql = "SELECT
							earnings.course_id,
							SUM($amount_condition) AS total_revenue
						FROM {$wpdb->tutor_earnings} earnings
						LEFT JOIN {$wpdb->tutor_orders} orders ON orders.id = earnings.order_id
						WHERE {$earning_where_clause}
						GROUP BY earnings.course_id";

		$enrollment_sql = QueryHelper::prepare_raw_query(
			"SELECT 
				post_parent AS course_id, 
				COUNT(DISTINCT post_author) AS total_student
			FROM {$wpdb->posts}
			WHERE {$enrollment_where_clause}
			GROUP BY post_parent",
			array()
		);

		//phpcs:disabled
		$result = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT
					post.ID AS course_id,
					post.post_title AS course_title,
					COALESCE(earnings.total_revenue, 0)  AS total_revenue,
					COALESCE(enrollments.total_student, 0) AS total_student
				FROM {$wpdb->posts} post
				INNER JOIN ({$earnings_sql}) earnings 
					ON earnings.course_id = post.ID
				LEFT JOIN ({$enrollment_sql}) enrollments 
					ON enrollments.course_id = post.ID
				WHERE post.post_type = %s
				ORDER BY {$order_by} DESC
				LIMIT %d",
				tutor()->course_post_type,
				$limit
			)
		);
		//phpcs:enable

		// If error occurred then throw new exception.
		if ( $wpdb->last_error ) {
			throw new \Exception( $wpdb->last_error ); //phpcs:ignore.
		}

		return $result;
	}

	/**
	 * Format top performing instructor courses for presentation.
	 *
	 * @since 4.0.0
	 *
	 * @param array $top_courses List of course objects returned from analytics.
	 *
	 * @return array Formatted top performing courses data.
	 */
	public static function format_instructor_top_performing_courses( $top_courses ) {

		if ( empty( $top_courses ) ) {
			return array();
		}

		return array_map(
			function ( $course ) {
				return array(
					'name'     => $course->course_title,
					'url'      => get_permalink( $course->course_id ),
					'revenue'  => tutor_utils()->tutor_price( $course->total_revenue ?? 0 ),
					'students' => $course->total_student ?? 0,
				);
			},
			$top_courses
		);
	}

	/**
	 * Retrieve upcoming live session tasks (Zoom / Google Meet) for an instructor.
	 *
	 * @since 4.0.0
	 *
	 * @param int $instructor_id Instructor (author) user ID.
	 *
	 * @return array List of upcoming live task posts.
	 */
	public static function get_instructor_upcoming_live_tasks( $instructor_id ) {

		$is_google_meet_enable = tutor_utils()->is_addon_enabled( 'google-meet' );
		$is_zoom_enable        = tutor_utils()->is_addon_enabled( 'tutor-zoom' );

		$meta_keys = array_filter(
			array(
				$is_google_meet_enable ? 'tutor-google-meet-start-datetime' : null,
				$is_zoom_enable ? '_tutor_zm_start_datetime' : null,
			)
		);

		$post_types = array_filter(
			array(
				$is_google_meet_enable ? tutor()->meet_post_type : null,
				$is_zoom_enable ? tutor()->zoom_post_type : null,
			)
		);

		if ( empty( $meta_keys ) ) {
			return array();
		}

		return get_posts(
			array(
				'post_type'   => $post_types,
				'post_status' => 'publish',
				'author'      => $instructor_id,
				'numberposts' => 5,
				'meta_query'  => array(
					array(
						'key'     => $meta_keys,
						'value'   => gmdate( 'Y-m-d H:i:s', strtotime( 'now' ) ),
						'compare' => '>=',
						'type'    => 'DATETIME',
					),
				),
			)
		);
	}

	/**
	 * Format upcoming instructor live tasks for presentation.
	 *
	 * @since 4.0.0
	 *
	 * @param array $upcoming_live_tasks List of live task post objects.
	 *
	 * @return array Formatted upcoming live tasks data.
	 */
	public static function format_instructor_upcoming_live_tasks( $upcoming_live_tasks ) {

		if ( empty( $upcoming_live_tasks ) ) {
			return array();
		}

		return array_map(
			function ( $task ) {

				$is_zoom = tutor()->zoom_post_type === $task->post_type;
				$is_meet = tutor()->meet_post_type === $task->post_type;

				$live_meta_key = $is_zoom ? '_tutor_zm_start_datetime'
								: ( $is_meet ? 'tutor-google-meet-start-datetime' : '' );

				$url = $is_zoom ? ( json_decode( get_post_meta( $task->ID, '_tutor_zm_data' )[0] )->join_url ?? '' )
								: ( $is_meet ? get_post_meta( $task->ID, 'tutor-google-meet-link', true ) : '' );

				$start_date = get_post_meta( $task->ID, $live_meta_key, true );

				return array(
					'name'      => $task->post_title,
					'date'      => wp_date( 'Y-m-d h:i A', strtotime( $start_date ) ),
					'url'       => $url,
					'post_type' => $task->post_type,
				);
			},
			$upcoming_live_tasks
		);
	}

	/**
	 * Format recent instructor reviews for display.
	 *
	 * @since 4.0.0
	 *
	 * @param array $reviews List of review objects.
	 *
	 * @return array Formatted recent reviews data.
	 */
	public static function format_instructor_recent_reviews( $reviews ) {

		if ( empty( $reviews ) ) {
			return array();
		}

		return array_map(
			function ( $review ) {
				return array(
					'user'        => array(
						'id'     => $review->user_id,
						'name'   => $review->display_name,
						'avatar' => tutor_utils()->get_user_avatar_url( $review->user_id ),
					),
					'course_name' => get_the_title( $review->comment_post_ID ),
					'date'        => $review->comment_date,
					'rating'      => $review->rating,
					'review_text' => $review->comment_content,
				);
			},
			$reviews
		);
	}

	/**
	 *
	 * Calculates percentage change and UI metadata for a statistics card.
	 *
	 * @since 4.0.0
	 *
	 * @param float $current_data  Current period value.
	 * @param float $previous_data Previous period value.
	 *
	 * @return array{
	 *     percentage: string,
	 *     icon: string,
	 *     class: string,
	 *     icon_class: string
	 * }
	 */
	public static function get_stat_card_details( float $current_data, float $previous_data ) {

		if ( empty( $previous_data ) && empty( $current_data ) ) {
			return array(
				'percentage' => '',
				'icon'       => Icon::MINUS,
				'class'      => 'tutor-text-primary',
			);
		}

		if ( empty( $previous_data ) ) {
			$percentage = 100;
		} else {
			$percentage = ( ( $current_data - $previous_data ) / $previous_data ) * 100;
		}

		$is_negative = $percentage < 0;
		$icon        = $is_negative ? Icon::ARROW_DOWN : Icon::ARROW_UP;
		$class       = $is_negative ? 'tutor-p2 tutor-actions-critical-primary' : 'tutor-p2 tutor-actions-success-primary';

		return array(
			'percentage' => number_format( abs( $percentage ), 2 ) . '%',
			'icon'       => $icon,
			'class'      => $class,
			'icon_class' => '-tutor-mb-1',
		);
	}

	/**
	 * Save the instructor home sections order for the current user.
	 *
	 * @since 4.0.0
	 *
	 * @return void Sends JSON success or error response and exits.
	 */
	public function ajax_save_home_sections_order() {
		tutor_utils()->check_nonce();

		if ( ! User::is_instructor() ) {
			$this->response_bad_request( tutor_utils()->error_message() );
		}

		$order = Input::post( 'order', array(), Input::TYPE_ARRAY );
		$order = array_values( array_map( 'sanitize_key', $order ) );

		update_user_meta( get_current_user_id(), '_tutor_instructor_home_sections_order', array_flip( $order ) );

		$this->response_success( __( 'Settings saved successfully', 'tutor' ) );
	}

	/**
	 * Save the visibility state of instructor home sections.
	 *
	 * @since 4.0.0
	 *
	 * @return void Sends JSON success or error response and exits.
	 */
	public function ajax_save_home_section_visibility() {
		tutor_utils()->check_nonce();

		if ( ! User::is_instructor() ) {
			$this->response_bad_request( tutor_utils()->error_message() );
		}

		$items = Input::post( 'items', '', Input::TYPE_STRING );
		$items = array_map( 'rest_sanitize_boolean', (array) json_decode( $items ) );

		update_user_meta( get_current_user_id(), '_tutor_instructor_home_sections_visibility', $items );

		$this->response_success( __( 'Settings saved successfully', 'tutor' ) );
	}
}